bundler-resolutions 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a9bc7c0b4191b262edcc52111b78fb8c67b74b0c9642ddec0036142d46c2989
4
- data.tar.gz: bb5c955b6db7a14e56e8d5048c98709a36f634f25364a14cbe22d6606d3faeab
3
+ metadata.gz: 7a6def9961c77861086b6b58177a934945938362e2125ba5dc49b8a6c06efdff
4
+ data.tar.gz: ccef554f38761f9916843d675f70ef7189ef8a7070bdb3d9b2e466e2eed998de
5
5
  SHA512:
6
- metadata.gz: 2d0f016d3a21dd98fe3d175d015fd4e526a514f32acf8283205db330ac9e52df4444e59a12e761882580790c8bccc1599739d3375291f598b810b4e7800b0705
7
- data.tar.gz: cf1c8bee485fa053ec3de1ac094638381d9153796de7e77f5add4ee31953f41889f3563f6413830a5351633cb2f7960d5557447f66d52aca69ac175293696646
6
+ metadata.gz: '01507905d1b9402c1efc92f9509b5aeef2b858d18c3b3e8555af937e1b479dc4b2711f87eed3ef87b8d64bfb7285bb299322a5925d43013e6dac6afa2128b5ba'
7
+ data.tar.gz: c85660a42a9149ffcded4f649ef8b2d9413da63eb435d9c8231be3fcdf26dc6aebea0a6608bd1206298ac3caf47b01681a573cecc4e92b0dc3b6638a893b219d
data/README.md CHANGED
@@ -5,7 +5,7 @@ bundler-resolutions
5
5
  [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
6
 
7
7
  [bundler-resolutions](https://github.com/hlascelles/bundler-resolutions) is a [bundler](https://bundler.io/)
8
- plugin that allows you to specify gem version requirements in your `Gemfile` without explicitly declaring
8
+ plugin that allows you to specify gem version requirements for your `Gemfile` without explicitly declaring
9
9
  a concrete dependency on those gems. It acts much like the
10
10
  [resolutions](https://classic.yarnpkg.com/lang/en/docs/selective-version-resolutions/) feature in
11
11
  [Yarn](https://yarnpkg.com/).
@@ -15,54 +15,68 @@ a concrete dependency on those gems. It acts much like the
15
15
 
16
16
  ## Usage
17
17
 
18
- Add `bundler-resolutions` to your Gemfile, and add a `resolutions` group to specify the gems you
19
- want to specify versions requirements for.
18
+ Add `bundler-resolutions` to your Gemfile, and add a `.bundler-resolutions.yml` file to
19
+ specify the gems you want to specify versions requirements for.
20
20
 
21
- The resulting `Gemfile.lock` in this example will have nokogiri locked to `1.16.5` or above.
21
+ ### Example 1
22
22
 
23
- ```ruby
24
- plugin 'bundler-resolutions'
25
-
26
- gem "rails"
23
+ In this example the resulting `Gemfile.lock` will have nokogiri locked to `1.16.5` or above, but
24
+ nokogiri will not be present in the `DEPENDENCIES` section of the lock file. Also, if `rails` were
25
+ to change to a version that did not depend on nokogiri, then the resolution would not be used or
26
+ appear in the lock file at all.
27
27
 
28
- group :resolutions do
29
- gem "nokogiri", ">= 1.16.5" # CVE-2024-34459
30
- end
28
+ `.bundler-resolutions.yml`:
29
+ ```yaml
30
+ gems:
31
+ nokogiri: ">= 1.16.5" # CVE-2024-34459
31
32
  ```
32
33
 
33
- However the `Gemfile.lock` from this example will not have nokogiri at all, as it is neither
34
- explicitly declared, nor brought in as a transitive dependency.
35
-
34
+ `Gemfile`:
36
35
  ```ruby
37
- plugin 'bundler-resolutions'
38
-
39
- group :resolutions do
40
- gem "nokogiri", ">= 1.16.5" # CVE-2024-34459
41
- end
36
+ gem 'bundler-resolutions'
37
+ gem "rails"
42
38
  ```
43
39
 
44
- ## Detail
40
+ ### Example 2
45
41
 
46
- `bundler-resolutions` allows you to specify version requirements using standard gem syntax in your
47
- Gemfile to indicate that you have version requirements for those gems *if* they were to be brought
48
- in as transitive dependencies, but that you don't depend on them yourself directly.
42
+ Here, the `Gemfile.lock` from this example will not have nokogiri at all, as it is neither
43
+ explicitly declared in the Gemfile, nor brought in as a transitive dependency.
49
44
 
50
- An example use case is in the Gemfile given below. Here we are saying that although we do not use nokogiri
51
- specifically ourselves, we want to ensure that if it is pulled in by other gems then it will
52
- always be above the know version with a CVE.
45
+ `.bundler-resolutions.yml`:
46
+ ```yaml
47
+ gems:
48
+ nokogiri: ">= 1.16.5" # CVE-2024-34459
49
+ ```
53
50
 
54
51
  ```ruby
55
- source "https://rubygems.org"
52
+ gem 'bundler-resolutions'
53
+ gem "thor"
54
+ ```
56
55
 
57
- plugin 'bundler-resolutions'
56
+ ## Config file
58
57
 
59
- gem "rails"
58
+ The config file is a YAML file with a `gems` key that contains a mapping of gem names to version
59
+ requirements. The version requirements are the same as those used in the `Gemfile`.
60
+
61
+ Example:
60
62
 
61
- group :resolutions do
62
- gem "nokogiri", ">= 1.16.5" # CVE-2024-34459
63
- end
63
+ ```yaml
64
+ gems:
65
+ nokogiri: ">= 1.16.5" # CVE-2024-34459
66
+ thor: ">= 1.0.1, < 2.0"
64
67
  ```
65
68
 
69
+ By default, `bundler-resolutions` will look for a file named `.bundler-resolutions.yml` in the
70
+ current directory, or the parent, and continue looking up to the root dir.
71
+
72
+ You can also specify a file location by setting the `BUNDLER_RESOLUTIONS_CONFIG` ENV var.
73
+
74
+ ## Detail
75
+
76
+ `bundler-resolutions` allows you to specify version requirements in a config file
77
+ to indicate that you have version requirements for those gems *if* they were to be brought
78
+ in as transitive dependencies, but that you don't depend on them yourself directly.
79
+
66
80
  The big difference between doing this and just declaring it in your Gemfile is that it will only
67
81
  be used in resolutions (and be written to your lock file) if the gems you do directly depend on
68
82
  continue to use it. If they stop using it, then your resolutions will take no part in the
@@ -73,19 +87,24 @@ present in the `DEPENDENCIES` section of the lock file, as it is not a direct de
73
87
 
74
88
  ## Use cases
75
89
 
76
- There are a number of reasons you may want to prevent the usage of some gem versions, without
77
- direct use, such as:
90
+ There are a number of reasons you may want to prevent the usage of some gem versions, but without
91
+ declaring their direct use in Gemfiles. Also there are reasons to set versions across a monorepo
92
+ of many Gemfiles, but where not all apps use all blessed versions, such as:
78
93
 
79
94
  1. You have learnt of a CVE of a gem.
80
- 2. You have internal processes that mandate the usage of certain gem versions for legal or sign off reasons.
81
- 3. You know of gem incompatibilities in later versions.
82
- 4. You know that different OS architectures do not work with some versions.
95
+ 1. You have internal processes that mandate the usage of certain gem versions for legal or sign off reasons.
96
+ 1. You wish to take a paranoid approach to updating certain high value target gems. eg `devise`.
97
+ 1. You want certain gem collections to move in lockstep. eg `sinatra`, `rack` and `rack-protection`, which are relatively tightly coupled.
98
+ 1. You know of gems that are very slow to install and you have preinstalled them in internal base images. eg `rugged` or `sorbet`.
99
+ 1. You know of gems that are tightly coupled to ruby itself that shouldn't be upgraded. eg `stringio` and `psych`.
100
+ 1. You know of gem incompatibilities with your codebase in their later versions.
101
+ 1. You know that different OS architectures do not work with some versions.
102
+ 1. You wish to prevent unintentional downgrades of dependencies when using `bundle` commands.
83
103
 
84
104
  ## How it works
85
105
 
86
- `bundler-resolutions` works by patching the Gemfile DSL to allow for special processing
87
- of the `resolutions` group. It also patches the bundler `filtered_versions_for` method to
88
- allow for the resolution restrictions from the versions specified in the `resolutions` group.
106
+ `bundler-resolutions` works by patching the `bundler` `Resolver` `filtered_versions_for` method to
107
+ allow for the resolution restrictions from the versions specified in the config file.
89
108
 
90
109
  This is a very early version, and it should be considered experimental.
91
110
 
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Bundler
6
+ class Resolutions
7
+ class Config
8
+ CONFIG_FILE_NAME = ".bundler-resolutions.yml"
9
+
10
+ class << self
11
+ def load_config(config = nil)
12
+ raw_hash = if config.is_a?(Hash)
13
+ config
14
+ else
15
+ YAML.safe_load_file(find_config(config))
16
+ end
17
+ gems = raw_hash.fetch("gems")
18
+ gems.transform_values { |version| Gem::Requirement.new(version.split(",")) }
19
+ end
20
+
21
+ private def find_config(config = nil)
22
+ return config if config # If present, assume it is a location
23
+
24
+ # Use the ENV if present
25
+ env_file = ENV["BUNDLER_RESOLUTIONS_CONFIG"]
26
+ return env_file if env_file
27
+
28
+ # Otherwise find it in the file tree
29
+ dir = Dir.pwd
30
+ until File.exist?(File.join(dir, CONFIG_FILE_NAME))
31
+ dir = File.dirname(dir)
32
+ raise "Could not find #{CONFIG_FILE_NAME}" if dir == "/"
33
+ end
34
+ File.join(dir, CONFIG_FILE_NAME)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -4,6 +4,6 @@ require_relative "../resolutions"
4
4
 
5
5
  module Bundler
6
6
  class Resolutions
7
- VERSION = "0.1.0"
7
+ VERSION = "0.3.0"
8
8
  end
9
9
  end
@@ -1,47 +1,114 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+ require_relative "resolutions/config"
5
+
3
6
  module Bundler
4
7
  class Resolutions
5
- GROUP_NAME = :resolutions
6
-
7
- attr_reader :resolutions
8
+ DEFAULT_GEM_REQUIREMENT = Gem::Requirement.default
8
9
 
9
- def initialize
10
- @resolutions = {}
11
- end
10
+ attr_reader :config
12
11
 
13
- # This method is called by the DSL to set the resolution for a given gem. It is effectively
14
- # an override of the normal gem method.
15
- def gem(name, requirements)
16
- resolutions[name.to_sym] = requirements
12
+ def initialize(config = nil)
13
+ @config = Bundler::Resolutions::Config.load_config(config)
17
14
  end
18
15
 
19
16
  class << self
20
- def instance = @instance ||= new
17
+ def instance
18
+ @instance ||= new
19
+ end
21
20
  end
22
21
 
23
22
  # A module we prepend to Bundler::Resolutions::Resolver
23
+ # :reek:ModuleInitialize
24
24
  module Resolver
25
+ # Override the initializer in the resolver
26
+ def initialize(*args)
27
+ Bundler::Resolutions.instance.add_concrete_resolutions_for(args.first)
28
+ super
29
+ end
30
+
25
31
  # This overrides the default behaviour of the resolver to filter out versions that don't
26
- # satisfy the requirements specified in RESOLVER_RESOLUTIONS.
32
+ # satisfy the requirements specified in .bundler-resolutions.yml.
27
33
  def filtered_versions_for(package)
28
- super.select do |pkg|
29
- req = Bundler::Resolutions.instance.resolutions[package.name.to_sym]
30
- req ? Gem::Requirement.new(*req.split(",")).satisfied_by?(pkg.version) : true
34
+ Bundler::Resolutions.instance.constrain_versions_for(super, package)
35
+ end
36
+ end
37
+
38
+ def constrain_versions_for(results, package)
39
+ results.select do |pkg|
40
+ req = resolutions_for(package.name)
41
+ if req
42
+ log("making sure #{package} is satisfied by #{req}")
43
+ req.satisfied_by?(pkg.version)
44
+ else
45
+ true
31
46
  end
32
47
  end
33
48
  end
34
49
 
35
- # A module we prepend to Bundler::Dsl
36
- module Dsl
37
- # Here we override the normal group function to capture the resolutions, and ensure any
38
- # gem calls are effectively a no-op as regards to adding them to dependencies.
39
- def group(*args, &blk)
40
- args.first == GROUP_NAME ? Bundler::Resolutions.instance.instance_eval(&blk) : super
50
+ def add_concrete_resolutions_for(base)
51
+ base.requirements.each do |bundler_dependency|
52
+ requirement_name = bundler_dependency.name
53
+ resolutions = resolutions_for(requirement_name)
54
+
55
+ if resolutions
56
+ log(<<~MSG, requirement_name)
57
+ has resolutions for concrete dependency '#{requirement_name}': #{resolutions}
58
+ MSG
59
+ else
60
+ log("has no resolutions for concrete dependency '#{requirement_name}'", requirement_name)
61
+ next
62
+ end
63
+
64
+ bundler_resolutions_reqs = resolutions.requirements
65
+ apply_resolutions_for_concrete_gem(bundler_dependency, bundler_resolutions_reqs)
66
+ end
67
+ end
68
+
69
+ private def resolutions_for(package_name)
70
+ config[package_name]
71
+ end
72
+
73
+ # You can debug with BUNDLER_RESOLUTIONS_DEBUG=gem_name or BUNDLER_RESOLUTIONS_DEBUG=true
74
+ # to see all messages.
75
+ private def log(message, gem = nil)
76
+ return unless ENV["BUNDLER_RESOLUTIONS_DEBUG"]
77
+ return if gem && !ENV["BUNDLER_RESOLUTIONS_DEBUG"].split(",").include?(gem)
78
+
79
+ puts "bundler-resolutions: #{message}"
80
+ end
81
+
82
+ private def apply_resolutions_for_concrete_gem(bundler_dependency, bundler_resolutions_reqs)
83
+ requirement_name = bundler_dependency.name
84
+ bundler_resolutions_reqs.each do |r|
85
+ # If the concrete requirement is already in the Gemfile, skip it
86
+ requirements = bundler_dependency.requirement.requirements
87
+ if requirements.include?(r)
88
+ # We don't want to double up / dupe the same requirements
89
+ log(<<~MSG, requirement_name)
90
+ Skipping adding requirements to gem concretely specified in Gemfile as it
91
+ was already present: #{requirement_name}: #{bundler_dependency}
92
+ MSG
93
+ next
94
+ end
95
+
96
+ # Otherwise add the additional requirement
97
+ before_req = bundler_dependency.to_s
98
+ # If there were no requirements before, there is a default one for ">= 0". We need to
99
+ # remove that so when we add the new one the implicit ">= 0" is not present, as it normally
100
+ # isn't written out to lockfiles.
101
+ requirements.clear if bundler_dependency.requirement == DEFAULT_GEM_REQUIREMENT
102
+ # Add the new requirement
103
+ requirements << r
104
+ after_req = bundler_dependency.to_s
105
+
106
+ log(<<~MSG, requirement_name)
107
+ Adding concrete constraints for #{requirement_name}. Before: #{before_req}. After: #{after_req}.
108
+ MSG
41
109
  end
42
110
  end
43
111
  end
44
112
  end
45
113
 
46
114
  Bundler::Resolver.prepend(Bundler::Resolutions::Resolver)
47
- Bundler::Dsl.prepend(Bundler::Resolutions::Dsl)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-resolutions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Lascelles
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-10-02 00:00:00.000000000 Z
10
+ date: 2025-05-03 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: A bundler plugin to enforce resolutions without specifying a concrete
14
13
  dependency
@@ -20,6 +19,7 @@ extra_rdoc_files: []
20
19
  files:
21
20
  - README.md
22
21
  - lib/bundler/resolutions.rb
22
+ - lib/bundler/resolutions/config.rb
23
23
  - lib/bundler/resolutions/version.rb
24
24
  - plugins.rb
25
25
  homepage: https://github.com/hlascelles/bundler-resolutions
@@ -32,7 +32,6 @@ metadata:
32
32
  source_code_uri: https://github.com/hlascelles/bundler-resolutions/
33
33
  bug_tracker_uri: https://github.com/hlascelles/bundler-resolutions/issues
34
34
  rubygems_mfa_required: 'true'
35
- post_install_message:
36
35
  rdoc_options: []
37
36
  require_paths:
38
37
  - lib
@@ -40,15 +39,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
40
39
  requirements:
41
40
  - - ">="
42
41
  - !ruby/object:Gem::Version
43
- version: '0'
42
+ version: '3.2'
44
43
  required_rubygems_version: !ruby/object:Gem::Requirement
45
44
  requirements:
46
45
  - - ">="
47
46
  - !ruby/object:Gem::Version
48
47
  version: '0'
49
48
  requirements: []
50
- rubygems_version: 3.5.11
51
- signing_key:
49
+ rubygems_version: 3.6.6
52
50
  specification_version: 4
53
51
  summary: A bundler plugin to enforce resolutions without specifying a concrete dependency
54
52
  test_files: []