bundler-resolutions 0.1.0 → 0.2.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: 25173fc2b7df9fe3efd07150e315f38c6584199627bef4ea1ec44ec44e3fbb2d
4
+ data.tar.gz: 7728a62a42e3c9244932cb568e6a918d745a2bf9003f61d62bfb6402061610df
5
5
  SHA512:
6
- metadata.gz: 2d0f016d3a21dd98fe3d175d015fd4e526a514f32acf8283205db330ac9e52df4444e59a12e761882580790c8bccc1599739d3375291f598b810b4e7800b0705
7
- data.tar.gz: cf1c8bee485fa053ec3de1ac094638381d9153796de7e77f5add4ee31953f41889f3563f6413830a5351633cb2f7960d5557447f66d52aca69ac175293696646
6
+ metadata.gz: dc87f9b2c8feea662f00ef0a7340aae3355c7b3e10864fb70214c970d600d7602e19745eb6882886b5fd84726371ce9caf2ef67ee5e8ff5e9b0f451ee3c4a2fc
7
+ data.tar.gz: 741b9f3f5b10ad0a896b0776a0d4807f6eef446e5941554f6b95c6530f0df99e4cfc50223f848f4371639abc1e0912b9c7f8ef9b440b22b1841e12fd6c8a7f59
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,53 +15,49 @@ 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"
56
-
57
- plugin 'bundler-resolutions'
52
+ gem 'bundler-resolutions'
53
+ gem "thor"
54
+ ```
58
55
 
59
- gem "rails"
56
+ ## Detail
60
57
 
61
- group :resolutions do
62
- gem "nokogiri", ">= 1.16.5" # CVE-2024-34459
63
- end
64
- ```
58
+ `bundler-resolutions` allows you to specify version requirements in a config file
59
+ to indicate that you have version requirements for those gems *if* they were to be brought
60
+ in as transitive dependencies, but that you don't depend on them yourself directly.
65
61
 
66
62
  The big difference between doing this and just declaring it in your Gemfile is that it will only
67
63
  be used in resolutions (and be written to your lock file) if the gems you do directly depend on
@@ -73,19 +69,24 @@ present in the `DEPENDENCIES` section of the lock file, as it is not a direct de
73
69
 
74
70
  ## Use cases
75
71
 
76
- There are a number of reasons you may want to prevent the usage of some gem versions, without
77
- direct use, such as:
72
+ There are a number of reasons you may want to prevent the usage of some gem versions, but without
73
+ declaring their direct use in Gemfiles. Also there are reasons to set versions across a monorepo
74
+ of many Gemfiles, but where not all apps use all blessed versions, such as:
78
75
 
79
76
  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.
77
+ 1. You have internal processes that mandate the usage of certain gem versions for legal or sign off reasons.
78
+ 1. You wish to take a paranoid approach to updating certain high value target gems. eg `devise`.
79
+ 1. You want certain gem collections to move in lockstep. eg `sinatra`, `rack` and `rack-protection`, which are relatively tightly coupled.
80
+ 1. You know of gems that are very slow to install and you have preinstalled them in internal base images. eg `rugged` or `sorbet`.
81
+ 1. You know of gems that are tightly coupled to ruby itself that shouldn't be upgraded. eg `stringio` and `psych`.
82
+ 1. You know of gem incompatibilities with your codebase in their later versions.
83
+ 1. You know that different OS architectures do not work with some versions.
84
+ 1. You wish to prevent unintentional downgrades of dependencies when using `bundle` commands.
83
85
 
84
86
  ## How it works
85
87
 
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.
88
+ `bundler-resolutions` works by patching the `bundler` `Resolver` `filtered_versions_for` method to
89
+ allow for the resolution restrictions from the versions specified in the config file.
89
90
 
90
91
  This is a very early version, and it should be considered experimental.
91
92
 
@@ -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.2.0"
8
8
  end
9
9
  end
@@ -1,47 +1,75 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "yaml"
4
+
3
5
  module Bundler
4
6
  class Resolutions
5
- GROUP_NAME = :resolutions
7
+ CONFIG_FILE_NAME = ".bundler-resolutions.yml"
6
8
 
7
9
  attr_reader :resolutions
8
10
 
9
- def initialize
10
- @resolutions = {}
11
- end
12
-
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
11
+ def initialize(config = nil)
12
+ load_config(config)
17
13
  end
18
14
 
19
15
  class << self
20
- def instance = @instance ||= new
16
+ def instance
17
+ @instance ||= new
18
+ end
21
19
  end
22
20
 
23
21
  # A module we prepend to Bundler::Resolutions::Resolver
24
22
  module Resolver
25
23
  # This overrides the default behaviour of the resolver to filter out versions that don't
26
- # satisfy the requirements specified in RESOLVER_RESOLUTIONS.
24
+ # satisfy the requirements specified in .bundler-resolutions.yml.
27
25
  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
26
+ Bundler::Resolutions.instance.constrain_versions_for(super, package)
27
+ end
28
+ end
29
+
30
+ def constrain_versions_for(results, package)
31
+ results.select do |pkg|
32
+ req = resolutions[package.name]
33
+ if req
34
+ if ENV["BUNDLER_RESOLUTIONS_DEBUG"] == "true"
35
+ puts "bundler-resolutions making sure #{package} is satisfied by #{req}"
36
+ end
37
+ req.satisfied_by?(pkg.version)
38
+ else
39
+ true
31
40
  end
32
41
  end
33
42
  end
34
43
 
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
44
+ private def load_config(config = nil)
45
+ # Safe load yaml file whose location is given by an argument, an ENV, and if no
46
+ # env given then work up dir hierarchy until found.
47
+ # The file should be called .bundler-resolutions.yml
48
+ raw_hash = if config.is_a?(Hash)
49
+ config
50
+ else
51
+ YAML.safe_load_file(find_config(config))
52
+ end
53
+ gems = raw_hash.fetch("gems")
54
+ @resolutions = gems.transform_values { |version| Gem::Requirement.new(version.split(",")) }
55
+ end
56
+
57
+ private def find_config(config = nil)
58
+ return config if config # If present, assume it is a location
59
+
60
+ # Use the ENV if present
61
+ env_file = ENV["BUNDLER_RESOLUTIONS_CONFIG"]
62
+ return env_file if env_file
63
+
64
+ # Otherwise find it in the file tree
65
+ dir = Dir.pwd
66
+ until File.exist?(File.join(dir, CONFIG_FILE_NAME))
67
+ dir = File.dirname(dir)
68
+ raise "Could not find #{CONFIG_FILE_NAME}" if dir == "/"
41
69
  end
70
+ File.join(dir, CONFIG_FILE_NAME)
42
71
  end
43
72
  end
44
73
  end
45
74
 
46
75
  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.2.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-03-23 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: A bundler plugin to enforce resolutions without specifying a concrete
14
13
  dependency
@@ -32,7 +31,6 @@ metadata:
32
31
  source_code_uri: https://github.com/hlascelles/bundler-resolutions/
33
32
  bug_tracker_uri: https://github.com/hlascelles/bundler-resolutions/issues
34
33
  rubygems_mfa_required: 'true'
35
- post_install_message:
36
34
  rdoc_options: []
37
35
  require_paths:
38
36
  - lib
@@ -47,8 +45,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
45
  - !ruby/object:Gem::Version
48
46
  version: '0'
49
47
  requirements: []
50
- rubygems_version: 3.5.11
51
- signing_key:
48
+ rubygems_version: 3.6.6
52
49
  specification_version: 4
53
50
  summary: A bundler plugin to enforce resolutions without specifying a concrete dependency
54
51
  test_files: []