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 +4 -4
- data/README.md +59 -40
- data/lib/bundler/resolutions/config.rb +39 -0
- data/lib/bundler/resolutions/version.rb +1 -1
- data/lib/bundler/resolutions.rb +89 -22
- metadata +5 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a6def9961c77861086b6b58177a934945938362e2125ba5dc49b8a6c06efdff
|
4
|
+
data.tar.gz: ccef554f38761f9916843d675f70ef7189ef8a7070bdb3d9b2e466e2eed998de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '01507905d1b9402c1efc92f9509b5aeef2b858d18c3b3e8555af937e1b479dc4b2711f87eed3ef87b8d64bfb7285bb299322a5925d43013e6dac6afa2128b5ba'
|
7
|
+
data.tar.gz: c85660a42a9149ffcded4f649ef8b2d9413da63eb435d9c8231be3fcdf26dc6aebea0a6608bd1206298ac3caf47b01681a573cecc4e92b0dc3b6638a893b219d
|
data/README.md
CHANGED
@@ -5,7 +5,7 @@ bundler-resolutions
|
|
5
5
|
[](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
|
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
|
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
|
-
|
21
|
+
### Example 1
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
28
|
+
`.bundler-resolutions.yml`:
|
29
|
+
```yaml
|
30
|
+
gems:
|
31
|
+
nokogiri: ">= 1.16.5" # CVE-2024-34459
|
31
32
|
```
|
32
33
|
|
33
|
-
|
34
|
-
explicitly declared, nor brought in as a transitive dependency.
|
35
|
-
|
34
|
+
`Gemfile`:
|
36
35
|
```ruby
|
37
|
-
|
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
|
-
|
40
|
+
### Example 2
|
45
41
|
|
46
|
-
`
|
47
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
45
|
+
`.bundler-resolutions.yml`:
|
46
|
+
```yaml
|
47
|
+
gems:
|
48
|
+
nokogiri: ">= 1.16.5" # CVE-2024-34459
|
49
|
+
```
|
53
50
|
|
54
51
|
```ruby
|
55
|
-
|
52
|
+
gem 'bundler-resolutions'
|
53
|
+
gem "thor"
|
54
|
+
```
|
56
55
|
|
57
|
-
|
56
|
+
## Config file
|
58
57
|
|
59
|
-
gem
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
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
|
-
|
81
|
-
|
82
|
-
|
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
|
87
|
-
|
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
|
data/lib/bundler/resolutions.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
7
|
-
attr_reader :resolutions
|
8
|
+
DEFAULT_GEM_REQUIREMENT = Gem::Requirement.default
|
8
9
|
|
9
|
-
|
10
|
-
@resolutions = {}
|
11
|
-
end
|
10
|
+
attr_reader :config
|
12
11
|
|
13
|
-
|
14
|
-
|
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
|
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
|
32
|
+
# satisfy the requirements specified in .bundler-resolutions.yml.
|
27
33
|
def filtered_versions_for(package)
|
28
|
-
super
|
29
|
-
|
30
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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.
|
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:
|
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: '
|
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.
|
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: []
|