bundler-overrule 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 211e077f4842c7fe2b966596c15fb5a368855c82c6b5c766e3d716f9cf330c59
4
+ data.tar.gz: caa8412b3e12623347571872404eb6e462fa0d7978fda22d5c9c0b0896a0e0a9
5
+ SHA512:
6
+ metadata.gz: 7d157303868882f4a3bccb4d931d9e4451fce5f535137a28ebaf0174788615eb3858db8ea3301dac0e89b461ac0eb0fd714bb37cca9431fe667efd29e7f8b62d
7
+ data.tar.gz: 759bf851a997b806005a4afa326ce520adce55fb0f267122ca5c1b5b03f3032e4d7ca20a7513e6f1fad9a752800eeb65f25f79f0a658537da6055e55b1cd59c5
data/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Planned
11
+
12
+ - `swap 'old', with: 'new'` — substitute an API-compatible fork published under a different name.
13
+ - Windows CI.
14
+
15
+ ## [0.2.0] - 2026-08-16
16
+
17
+ Initial public release. Ships `force` and `ban` together, because the documented
18
+ quickstart uses both.
19
+
20
+ ### Added
21
+
22
+ - `force NAME, REQUIREMENT, reason: nil` — rewrites every dependency edge pointing at `NAME`,
23
+ from other gems' gemspecs and from your own Gemfile.
24
+ - `ban NAME, reason: nil` — removes a gem from resolution entirely.
25
+ - `overrule do ... end` block form for grouping rules.
26
+ - `bundle overrule list` — active rules and the exact constraints each one rewrote.
27
+ - `bundle overrule doctor` — flags stale rules, an unsupported Bundler, and a missing
28
+ bootstrap line. Exits non-zero on problems, so it works as a CI check.
29
+ - A one-line-per-rule summary printed on every `bundle install` / `bundle update`, including
30
+ which gem's constraint was overridden and the `reason:` you gave.
31
+ - `.bundle/overrule-report.json` recording the rules and rewritten edges of the last
32
+ resolution. Nothing overrule-specific is ever written into `Gemfile.lock`.
33
+
34
+ ### Behavior notes
35
+
36
+ - With no rules declared, zero patches are applied and resolution is byte-identical to
37
+ vanilla Bundler.
38
+ - Two conflicting rules for one gem is a hard error; an identical re-declaration is not
39
+ (Bundler evaluates the Gemfile more than once per run).
40
+ - Banning a gem the Gemfile requires directly is a hard error.
41
+ - Editing a rule forces a re-resolve even when the Gemfile's own dependencies are unchanged,
42
+ which Bundler would otherwise skip.
43
+ - Development dependencies are never rewritten or dropped.
44
+ - An unsupported Bundler aborts before resolution rather than patching internals it does not
45
+ recognise.
46
+
47
+ [Unreleased]: https://github.com/TheSoloHacker47/bundler-overrule/compare/v0.2.0...HEAD
48
+ [0.2.0]: https://github.com/TheSoloHacker47/bundler-overrule/releases/tag/v0.2.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nikhil Nelson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # bundler-overrule
2
+
3
+ > Force, ban, and swap gem versions in your Gemfile — the dependency overrides that Cargo (`[patch]`), npm (`overrides`), and yarn (`resolutions`) have had for years, now for Bundler.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/bundler-overrule.svg)](https://rubygems.org/gems/bundler-overrule)
6
+ [![CI](https://github.com/TheSoloHacker47/bundler-overrule/actions/workflows/ci.yml/badge.svg)](https://github.com/TheSoloHacker47/bundler-overrule/actions)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ ## The problem
10
+
11
+ You try to upgrade one gem:
12
+
13
+ ```
14
+ $ bundle update openssl
15
+ # ...nothing happens
16
+ ```
17
+
18
+ Because somewhere in your Gemfile, `web-push` pins `openssl ~> 2.2`. Upgrading `web-push`
19
+ requires `jwt ~> 3.0`, which conflicts with four other gems pinned to `jwt ~> 2.0` — some of
20
+ which haven't shipped a release in years. Your options today:
21
+
22
+ - fork every blocking gem just to relax a one-line version constraint, and maintain those forks forever, or
23
+ - point your Gemfile at git repos you don't control, or
24
+ - give up.
25
+
26
+ Pessimistic upper bounds (`~>`) are guesses about versions that didn't exist when they were
27
+ written — and they're very often wrong. **Your Gemfile is your domain.** You should get the
28
+ final say.
29
+
30
+ ## The fix
31
+
32
+ ```ruby
33
+ # Gemfile
34
+ source 'https://rubygems.org'
35
+
36
+ plugin 'bundler-overrule'
37
+ if (paths = Bundler::Plugin.index.load_paths('bundler-overrule'))
38
+ $LOAD_PATH.unshift(*paths)
39
+ require 'bundler-overrule'
40
+ end
41
+
42
+ gem 'rails'
43
+ gem 'web-push'
44
+
45
+ if defined?(Bundler::Overrule)
46
+ force 'openssl', '>= 3.0',
47
+ reason: 'web-push pins ~> 2.2 but works fine with 3.x (verified in CI)'
48
+
49
+ ban 'httpclient',
50
+ reason: 'unmaintained since 2016; vendored root certs expired'
51
+ end
52
+ ```
53
+
54
+ ```
55
+ $ bundle install
56
+ [bundler-overrule] 2 rules active — YOU own the consequences of these overrides:
57
+ force openssl >= 3.0 (was: web-push → ~> 2.2) # web-push pins ~> 2.2 but works fine with 3.x
58
+ ban httpclient (dropped from: jira-ruby) # unmaintained since 2016; vendored root certs expired
59
+ ```
60
+
61
+ Done. No forks. No `github:` pins. One honest, self-documenting line per override.
62
+
63
+ ## Installation
64
+
65
+ ```
66
+ $ bundle plugin install bundler-overrule
67
+ ```
68
+
69
+ Then add the bootstrap block shown above to the top of your Gemfile.
70
+
71
+ Requires **Ruby >= 3.1** and **Bundler >= 2.6**. Full tested matrix in [CI](.github/workflows/ci.yml).
72
+
73
+ ### Why the Gemfile needs those extra lines
74
+
75
+ Bundler plugins can't extend the Gemfile DSL on their own, so the `require` line is what makes
76
+ `force` and `ban` available while your Gemfile is being evaluated. Two details matter:
77
+
78
+ - **Guard the `require`.** `Bundler::Plugin.index.load_paths` returns `nil` — not an empty
79
+ array — when the plugin isn't installed. The `if` is what turns "plugin missing" into a
80
+ quiet no-op instead of a crash. Do **not** replace it with a trailing `rescue nil`: that
81
+ swallows real errors too, and you would get zero overrides with zero warning.
82
+ - **Keep the `if defined?` guard.** On the very first `bundle install`, Bundler evaluates your
83
+ whole Gemfile once *before* installing any plugin, just to find the `plugin` lines. During
84
+ that pass `force`/`ban` don't exist yet. The guard skips them; the real evaluation pass a
85
+ moment later has the plugin loaded and applies them.
86
+
87
+ ## Usage
88
+
89
+ ### `force NAME, REQUIREMENT, reason: nil`
90
+
91
+ Overrides **every** constraint on `NAME` — from other gems' gemspecs *and* from your own
92
+ Gemfile — with `REQUIREMENT`. Bundler resolves as if everyone had asked for your version.
93
+
94
+ ```ruby
95
+ force 'nokogiri', '~> 1.18'
96
+ force 'rack', '>= 3.0', reason: 'CVE-2025-XXXXX; sinatra pin is stale'
97
+ ```
98
+
99
+ ### `ban NAME, reason: nil`
100
+
101
+ Removes `NAME` from dependency resolution entirely. Useful for transitively-pulled gems you
102
+ know your code paths never hit, or abandoned gems you've replaced.
103
+
104
+ ```ruby
105
+ ban 'mimemagic', reason: 'we use marcel'
106
+ ```
107
+
108
+ Banning a gem your Gemfile requires directly is an error — remove the `gem` line instead.
109
+
110
+ ### Block form
111
+
112
+ Sugar for grouping; same registry underneath.
113
+
114
+ ```ruby
115
+ overrule do
116
+ force 'openssl', '>= 3.0'
117
+ ban 'httpclient'
118
+ end
119
+ ```
120
+
121
+ ### `swap OLD, with: NEW` *(v0.3 — roadmap)*
122
+
123
+ Substitute a maintained, API-compatible fork published under a different name:
124
+
125
+ ```ruby
126
+ swap 'httpclient', with: 'byroot-httpclient'
127
+ ```
128
+
129
+ ### Inspecting active rules
130
+
131
+ ```
132
+ $ bundle overrule list # every rule + exactly which constraints it rewrote
133
+ $ bundle overrule doctor # stale rules, unsupported Bundler, missing bootstrap line
134
+ ```
135
+
136
+ `doctor` exits non-zero when it finds a problem, so it works as a CI check.
137
+
138
+ ## How it works
139
+
140
+ `bundler-overrule` is a standard [Bundler plugin](https://bundler.io/guides/bundler_plugins.html).
141
+ When (and only when) you declare a rule, it filters the dependency lists that gem specifications
142
+ report to Bundler's resolver: `force` rewrites matching dependency requirements, `ban` drops
143
+ them. The resolver itself is untouched, and your `Gemfile.lock` is a perfectly ordinary
144
+ lockfile — deployment mode, `--frozen`, and CI caching all work unchanged.
145
+
146
+ With no rules declared, the plugin applies **zero** patches and your resolution is byte-identical
147
+ to vanilla Bundler (we test this — see scenario B7).
148
+
149
+ Each run writes `.bundle/overrule-report.json` recording which edges were rewritten. That file
150
+ is what `list` and `doctor` read; it is safe to delete and safe to gitignore. Nothing
151
+ overrule-specific is ever written into `Gemfile.lock`.
152
+
153
+ ## Use responsibly
154
+
155
+ An override is you telling Bundler "I know better than this gem's author." Sometimes you do —
156
+ upper bounds are written before the future exists. But:
157
+
158
+ - **Test the result.** You are opting out of the maintainer's compatibility promise.
159
+ - **Write the `reason:`.** Your future self and your teammates will thank you.
160
+ - **Don't report bugs upstream** for gem combinations their maintainers never declared support
161
+ for — reproduce without the override first.
162
+ - Prefer deleting a rule once upstream relaxes the constraint (`bundle overrule doctor` flags
163
+ rules that no longer have any effect).
164
+
165
+ ## Compatibility
166
+
167
+ | | |
168
+ |---|---|
169
+ | Ruby | 3.1, 3.2, 3.3, 3.4 |
170
+ | Bundler | 2.6, 2.7, 4.0 (weekly CI run against latest) |
171
+ | OS | Linux, macOS (Windows: best effort, CI planned) |
172
+
173
+ Bundler 2.4 and 2.5 are not supported: they lack the internals this plugin relies on. There is
174
+ no Bundler 3.x — the line went 2.7 → 4.0.
175
+
176
+ If a new Bundler release moves the internals we patch, the plugin fails loudly with a clear
177
+ message rather than silently mis-resolving. Pin your Bundler version in CI like you already should.
178
+
179
+ ## Roadmap
180
+
181
+ - [x] v0.1 — `force`, warnings, `overrule list`
182
+ - [x] v0.2 — `ban`, `overrule doctor`
183
+ - [ ] v0.3 — `swap` (gem substitution)
184
+ - [ ] Windows CI
185
+
186
+ ## Prior art & credits
187
+
188
+ - [byroot's "The Missing Bundler Features"](https://byroot.github.io/ruby/bundler/2026/04/20/bundle-features.html) — this gem implements the `force`/ban/substitute semantics proposed there.
189
+ - Longstanding Bundler feature requests: rubygems/bundler#1549 (2011), #2412 (2013), rubygems/rfcs#54.
190
+ - [bundler-override](https://github.com/tarnowsc/bundler-override) and ManageIQ's bundler-inject — earlier takes on the same pain.
191
+ - Cargo `[patch]`, npm `overrides`, pnpm/yarn `resolutions` — proof this belongs in every package manager.
192
+
193
+ ## Contributing
194
+
195
+ Bug reports and PRs welcome — see [CONTRIBUTING.md](CONTRIBUTING.md). Quick start:
196
+
197
+ ```
198
+ $ git clone https://github.com/TheSoloHacker47/bundler-overrule && cd bundler-overrule
199
+ $ bin/setup
200
+ $ bundle exec rake spec integration
201
+ ```
202
+
203
+ ## License
204
+
205
+ [MIT](LICENSE).
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Overrule
5
+ # `bundle overrule list` / `bundle overrule doctor`.
6
+ #
7
+ # Bundler hands plugin commands the raw ARGV tail, so option parsing is
8
+ # ours to do. There is very little of it on purpose.
9
+ class Command
10
+ SUBCOMMANDS = %w[list doctor].freeze
11
+
12
+ USAGE = <<~USAGE
13
+ Usage: bundle overrule SUBCOMMAND
14
+
15
+ Subcommands:
16
+ list show every active rule and which dependency edges it rewrote
17
+ doctor check for stale rules, an unsupported Bundler, a missing bootstrap line
18
+
19
+ Options:
20
+ -h, --help show this message
21
+ -v, --version show the bundler-overrule version
22
+ USAGE
23
+
24
+ def initialize(registry: nil, reporter: nil, out: $stdout)
25
+ @registry = registry
26
+ @reporter = reporter
27
+ @out = out
28
+ end
29
+
30
+ # @return [Integer] process exit status
31
+ def call(args)
32
+ subcommand = Array(args).first
33
+
34
+ return help(0) if subcommand.nil? || ["-h", "--help"].include?(subcommand)
35
+ return print_version if ["-v", "--version"].include?(subcommand)
36
+ return unknown_subcommand(subcommand) unless SUBCOMMANDS.include?(subcommand)
37
+
38
+ load_gemfile_rules
39
+ dispatch(subcommand)
40
+ end
41
+
42
+ private
43
+
44
+ def dispatch(subcommand)
45
+ case subcommand
46
+ when "list"
47
+ emit(reporter.list)
48
+ 0
49
+ when "doctor"
50
+ emit(reporter.doctor)
51
+ reporter.healthy? ? 0 : 1
52
+ end
53
+ end
54
+
55
+ def emit(lines)
56
+ lines.each { |line| @out.puts(line) }
57
+ end
58
+
59
+ def print_version
60
+ @out.puts "bundler-overrule #{Overrule::VERSION}"
61
+ 0
62
+ end
63
+
64
+ def unknown_subcommand(subcommand)
65
+ @out.puts "Unknown subcommand `#{subcommand}`."
66
+ @out.puts
67
+ help(1)
68
+ end
69
+
70
+ def registry
71
+ @registry ||= Overrule.registry
72
+ end
73
+
74
+ def reporter
75
+ @reporter ||= Overrule.reporter
76
+ end
77
+
78
+ def help(status)
79
+ @out.puts USAGE
80
+ status
81
+ end
82
+
83
+ # Evaluating the Gemfile is what registers the rules. Without it `list`
84
+ # could only ever report what the state file remembered.
85
+ def load_gemfile_rules
86
+ Overrule.suppress_summary!
87
+ return if registry.any?
88
+
89
+ ::Bundler.definition
90
+ rescue StandardError => e
91
+ @out.puts "[bundler-overrule] could not evaluate the Gemfile: #{e.message}"
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Overrule
5
+ # One dependency edge that a rule actually changed.
6
+ #
7
+ # +owner+ is the gem whose gemspec declared the edge, or "Gemfile" for a
8
+ # direct dependency. +was+ / +now+ are requirement strings; +now+ is nil
9
+ # for a ban, because the edge is gone.
10
+ Edge = Struct.new(:rule, :owner, :was, :now) do
11
+ def banned?
12
+ now.nil?
13
+ end
14
+
15
+ def to_h
16
+ { "type" => rule.type.to_s, "gem" => rule.name, "owner" => owner,
17
+ "was" => was, "now" => now, "reason" => rule.reason }
18
+ end
19
+ end
20
+
21
+ # The heart of the gem: given a list of Gem::Dependency objects and a set
22
+ # of rules, return the rewritten list plus the edges that changed.
23
+ #
24
+ # Deliberately pure — no Bundler objects, no global state, no I/O — so the
25
+ # behavior contract can be unit-tested without running Bundler at all.
26
+ # Both the resolution seam and the installation seam call this same
27
+ # function, which is what keeps the lockfile and the installed gems from
28
+ # ever disagreeing about what a dependency list contains.
29
+ module DependencyRewriter
30
+ module_function
31
+
32
+ # @param dependencies [Array<Gem::Dependency>]
33
+ # @param rules_by_name [Hash{String => Rule}]
34
+ # @param owner [String, nil] gem name that declared these dependencies
35
+ # @return [Array(Array<Gem::Dependency>, Array<Edge>)]
36
+ def rewrite(dependencies, rules_by_name, owner: nil)
37
+ return [dependencies, []] if rules_by_name.empty? || dependencies.nil? || dependencies.empty?
38
+
39
+ edges = []
40
+ rewritten = []
41
+
42
+ dependencies.each do |dependency|
43
+ rule = rules_by_name[dependency.name]
44
+ replacement, edge = apply_rule(dependency, rule, owner)
45
+
46
+ rewritten << replacement unless replacement.nil?
47
+ edges << edge if edge
48
+ end
49
+
50
+ [rewritten, edges]
51
+ end
52
+
53
+ # Decide what one dependency becomes under one rule.
54
+ #
55
+ # @return [Array(Gem::Dependency, Edge)] the replacement (nil to drop the
56
+ # dependency entirely) and the edge to record (nil if nothing changed)
57
+ def apply_rule(dependency, rule, owner)
58
+ # Development dependencies never reach the resolver, but spec objects
59
+ # still report them. Rewriting or dropping one would be a lie about
60
+ # what the gem declares, for no benefit.
61
+ return [dependency, nil] if rule.nil? || development?(dependency)
62
+
63
+ case rule.type
64
+ when :ban
65
+ [nil, Edge.new(rule, owner, requirement_string(dependency), nil)]
66
+ when :force
67
+ return [dependency, nil] if dependency.requirement == rule.requirement
68
+
69
+ [replace(dependency, rule.requirement),
70
+ Edge.new(rule, owner, requirement_string(dependency), rule.requirement.to_s)]
71
+ else
72
+ [dependency, nil]
73
+ end
74
+ end
75
+
76
+ def development?(dependency)
77
+ dependency.respond_to?(:type) && dependency.type == :development
78
+ end
79
+
80
+ def requirement_string(dependency)
81
+ dependency.requirement.to_s
82
+ end
83
+
84
+ # Swap in a new requirement while keeping everything else about the
85
+ # dependency intact.
86
+ #
87
+ # We copy-and-mutate rather than construct a fresh object on purpose.
88
+ # Bundler::Dependency derives #groups, #source, #git, #path, #github,
89
+ # #branch and #ref lazily from an options hash it keeps privately, and
90
+ # the shape of that hash has changed across Bundler versions. Rebuilding
91
+ # from public readers silently drops whichever of those we forgot — a
92
+ # `force` on a gem declared with `git:` would quietly lose its source.
93
+ # Duplicating preserves the exact class and every option, forever.
94
+ def replace(dependency, requirement)
95
+ replacement = dependency.dup
96
+ replacement.instance_variable_set(:@requirement, requirement)
97
+ replacement
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module Overrule
5
+ # The Gemfile DSL. Prepended into Bundler::Dsl by the bootstrap file, which
6
+ # is why `force` and `ban` are callable at the top level of a Gemfile.
7
+ module Dsl
8
+ # Override every constraint on +name+ — from other gems' gemspecs and
9
+ # from your own Gemfile — with +requirement+.
10
+ #
11
+ # force 'openssl', '>= 3.0', reason: 'web-push pins ~> 2.2; 3.x is fine'
12
+ def force(name, requirement, reason: nil)
13
+ Overrule.force(name, requirement, reason: reason)
14
+ end
15
+
16
+ # Remove +name+ from dependency resolution entirely.
17
+ #
18
+ # ban 'httpclient', reason: 'unmaintained; vendored certs expired'
19
+ def ban(name, reason: nil)
20
+ Overrule.ban(name, reason: reason)
21
+ end
22
+
23
+ # Sugar for grouping rules. Same registry underneath.
24
+ #
25
+ # overrule do
26
+ # force 'openssl', '>= 3.0'
27
+ # ban 'httpclient'
28
+ # end
29
+ def overrule(&block)
30
+ raise ArgumentError, "overrule requires a block" unless block
31
+
32
+ instance_eval(&block)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ module Bundler
6
+ module Overrule
7
+ # Base class for every error this plugin raises.
8
+ #
9
+ # Inherits Bundler::BundlerError so Bundler's friendly-error handler prints
10
+ # the message on its own, without a Ruby backtrace — these are all user
11
+ # mistakes in a Gemfile, not crashes.
12
+ class Error < ::Bundler::BundlerError
13
+ def status_code
14
+ 59
15
+ end
16
+ end
17
+
18
+ # Raised when a rule is malformed: a bad requirement string, a blank gem
19
+ # name, an unusable +reason:+.
20
+ class InvalidRuleError < Error; end
21
+
22
+ # Raised when two conflicting rules target the same gem (B4).
23
+ class DuplicateRuleError < Error; end
24
+
25
+ # Raised when a `ban` targets a gem the Gemfile requires directly (B6).
26
+ class BannedDirectDependencyError < Error; end
27
+
28
+ # Raised when the running Bundler does not expose the internals we patch.
29
+ # We would rather abort loudly than silently mis-resolve (G5, B9).
30
+ class UnsupportedBundlerError < Error
31
+ def initialize(detail)
32
+ super(<<~MSG)
33
+ bundler-overrule does not support Bundler #{::Bundler::VERSION}.
34
+
35
+ #{detail}
36
+
37
+ Supported: Bundler #{Overrule::SUPPORTED_BUNDLER_DESCRIPTION}.
38
+
39
+ This is a deliberate hard stop. bundler-overrule rewrites dependency
40
+ edges by patching Bundler internals; if those internals have moved,
41
+ continuing could silently resolve the wrong versions. Pin your Bundler
42
+ version, or upgrade bundler-overrule if a newer release supports this one.
43
+ MSG
44
+ end
45
+ end
46
+ end
47
+ end