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.
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+
5
+ require_relative "overrule/version"
6
+ require_relative "overrule/errors"
7
+ require_relative "overrule/rule"
8
+ require_relative "overrule/dependency_rewriter"
9
+ require_relative "overrule/registry"
10
+ require_relative "overrule/reporter"
11
+ require_relative "overrule/patcher"
12
+ require_relative "overrule/dsl"
13
+
14
+ module Bundler
15
+ # Force, ban, and swap gem versions from your Gemfile.
16
+ #
17
+ # Nothing here patches Bundler until the first rule is declared: with no
18
+ # rules, resolution is byte-identical to vanilla Bundler (G4).
19
+ module Overrule
20
+ # Verified against 2.6.x, 2.7.x and 4.0.x. The upper bound is deliberately
21
+ # loose — Patcher also structurally verifies every patch target, which is a
22
+ # better guard than a version number, and refuses to run if one is missing.
23
+ #
24
+ # An Array, not a comma-joined String: Gem::Requirement.create(">= 2.6, < 5")
25
+ # raises BadRequirementError.
26
+ SUPPORTED_BUNDLER = [">= 2.6", "< 5"].freeze
27
+ SUPPORTED_BUNDLER_DESCRIPTION = SUPPORTED_BUNDLER.join(", ").freeze
28
+
29
+ class << self
30
+ def registry
31
+ @registry ||= Registry.new
32
+ end
33
+
34
+ def reporter
35
+ @reporter ||= Reporter.new(registry)
36
+ end
37
+
38
+ # Test hook.
39
+ def reset!
40
+ @registry = nil
41
+ @reporter = nil
42
+ @rules_changed = nil
43
+ @finalized = false
44
+ @definition_built = false
45
+ @suppress_summary = false
46
+ Patcher.reset!
47
+ self
48
+ end
49
+
50
+ def force(name, requirement, reason: nil)
51
+ register(Force.new(name, requirement, reason: reason))
52
+ end
53
+
54
+ def ban(name, reason: nil)
55
+ register(Ban.new(name, reason: reason))
56
+ end
57
+
58
+ def register(rule)
59
+ rule = registry.add(rule)
60
+ Patcher.apply!
61
+ rule
62
+ end
63
+
64
+ # Called from the Dsl#to_definition seam with the Gemfile's own
65
+ # dependency array, which we mutate in place.
66
+ #
67
+ # `force` has to beat a Gemfile pin (B2), and `ban` of a directly
68
+ # required gem is a user error, not something to silently honour (B6).
69
+ def rewrite_gemfile_dependencies!(dependencies)
70
+ return dependencies if registry.empty? || dependencies.nil?
71
+
72
+ reject_banned_direct_dependencies!(dependencies)
73
+ apply_forces_to_gemfile!(dependencies)
74
+ dependencies
75
+ end
76
+
77
+ # Has the rule set changed since the lockfile was last written? Used to
78
+ # defeat Bundler's "lockfile looks current, skip resolution" fast path.
79
+ def rules_changed?
80
+ return false if registry.empty?
81
+ return @rules_changed unless @rules_changed.nil?
82
+
83
+ state = reporter.read_state
84
+ @rules_changed = state.nil? || state["fingerprint"] != registry.fingerprint
85
+ end
86
+
87
+ # True while Bundler is doing its pre-pass over the Gemfile looking for
88
+ # `plugin` lines (Bundler::Plugin.gemfile_install). Nothing is resolved
89
+ # in that pass.
90
+ def plugin_prepass?(dsl)
91
+ defined?(::Bundler::Plugin::DSL) && dsl.is_a?(::Bundler::Plugin::DSL)
92
+ end
93
+
94
+ def note_definition_built!
95
+ @definition_built = true
96
+ end
97
+
98
+ # `bundle overrule list` prints its own report; the install-time "YOU own
99
+ # the consequences" banner on top of it would just be noise.
100
+ def suppress_summary!
101
+ @suppress_summary = true
102
+ end
103
+
104
+ def suppress_summary?
105
+ @suppress_summary ||= false
106
+ end
107
+
108
+ def definition_built?
109
+ @definition_built ||= false
110
+ end
111
+
112
+ # Print the summary and persist the state file. Idempotent — it is
113
+ # reachable from both the plugin hook and the at_exit fallback.
114
+ def finalize!
115
+ return if @finalized || registry.empty? || !definition_built?
116
+
117
+ @finalized = true
118
+ reporter.print_summary unless suppress_summary?
119
+ reporter.write_state!
120
+ rescue StandardError => e
121
+ # Reporting must never be the thing that breaks an install.
122
+ ::Bundler.ui.warn("[bundler-overrule] could not write report: #{e.message}")
123
+ end
124
+
125
+ def active?
126
+ registry.any?
127
+ end
128
+
129
+ private
130
+
131
+ def reject_banned_direct_dependencies!(dependencies)
132
+ registry.bans.each do |rule|
133
+ next unless dependencies.any? { |dep| dep.name == rule.name }
134
+
135
+ raise BannedDirectDependencyError, <<~MSG
136
+ you can't ban `#{rule.name}` — your Gemfile requires it directly.
137
+
138
+ Remove the `gem '#{rule.name}'` line instead. `ban` is for gems pulled in
139
+ transitively by your other dependencies.
140
+ MSG
141
+ end
142
+ end
143
+
144
+ def apply_forces_to_gemfile!(dependencies)
145
+ registry.forces.each do |rule|
146
+ index = dependencies.index { |dep| dep.name == rule.name }
147
+ next if index.nil?
148
+
149
+ existing = dependencies[index]
150
+ next if existing.requirement == rule.requirement
151
+
152
+ registry.filter([existing], owner: "Gemfile")
153
+ dependencies[index] = DependencyRewriter.replace(existing, rule.requirement)
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bootstrap file — this is what a Gemfile requires.
4
+ #
5
+ # plugin 'bundler-overrule'
6
+ # if (paths = Bundler::Plugin.index.load_paths('bundler-overrule'))
7
+ # $LOAD_PATH.unshift(*paths)
8
+ # require 'bundler-overrule'
9
+ # end
10
+ #
11
+ # Requiring this only makes `force` / `ban` available. No Bundler internals are
12
+ # patched until a rule is actually declared.
13
+
14
+ require_relative "bundler/overrule"
15
+
16
+ Bundler::Dsl.prepend(Bundler::Overrule::Dsl) unless Bundler::Dsl.ancestors.include?(Bundler::Overrule::Dsl)
17
+
18
+ # The rule summary is printed here, at exit, rather than from a Bundler plugin
19
+ # hook. Bundler's earliest install-time hook (`before-install-all`) fires after
20
+ # resolution but before installation, and some dependency edges are only read
21
+ # during installation — reporting there would under-report which constraints
22
+ # were actually rewritten. This runs for every Bundler command, including the
23
+ # ones that never fire plugin hooks at all (`bundle lock`, `bundle check`).
24
+ at_exit do
25
+ # $! is set when we are unwinding from an exception — a failed run should not
26
+ # be summarised as though the overrides took effect. Deliberately not using
27
+ # the English alias: this file is loaded during Gemfile evaluation in every
28
+ # bundle command, and it should require nothing it does not have to.
29
+ # rubocop:disable Style/SpecialGlobalVars
30
+ Bundler::Overrule.finalize! if $!.nil? && Bundler::Overrule.active?
31
+ # rubocop:enable Style/SpecialGlobalVars
32
+ end
data/plugins.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Bundler plugin entrypoint. Bundler `load`s this file at plugin-install time
4
+ # to discover which commands/hooks we register, and again at runtime whenever
5
+ # one of them is needed.
6
+ #
7
+ # Note what is NOT here: the dependency-rewriting patches. No Bundler plugin
8
+ # event fires before resolution, so hooks cannot influence it. The patches are
9
+ # activated by the `require 'bundler-overrule'` line in the user's Gemfile,
10
+ # which runs during Gemfile evaluation — before the resolver starts.
11
+
12
+ require "bundler/plugin/api"
13
+
14
+ # Registers `bundle overrule list` / `bundle overrule doctor`.
15
+ class BundlerOverruleCommand < Bundler::Plugin::API
16
+ command "overrule"
17
+
18
+ def exec(_command, args)
19
+ require "bundler-overrule"
20
+ require "bundler/overrule/command"
21
+
22
+ exit(Bundler::Overrule::Command.new.call(args))
23
+ end
24
+ end
25
+
26
+ # Deliberately no hooks.
27
+ #
28
+ # `before-install-all` looks like the right place to print the rule summary —
29
+ # it fires after resolution, before anything installs. It is not: some
30
+ # dependency edges are only read during installation itself, so a summary
31
+ # printed there under-reports and can claim a rule "matched nothing" when it in
32
+ # fact rewrote the graph. The summary is emitted from an at_exit handler in
33
+ # lib/bundler-overrule.rb instead, where the picture is complete.
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bundler-overrule
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Nikhil Nelson
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ bundler-overrule is a Bundler plugin that gives application developers the final
14
+ say over their dependency graph. It adds `force` and `ban` to the Gemfile DSL:
15
+ `force` rewrites every version constraint on a gem, from other gems' gemspecs and
16
+ from your own Gemfile; `ban` removes a gem from resolution entirely. This is the
17
+ Ruby equivalent of Cargo's [patch], npm's overrides, and yarn's resolutions.
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE
24
+ - README.md
25
+ - lib/bundler-overrule.rb
26
+ - lib/bundler/overrule.rb
27
+ - lib/bundler/overrule/command.rb
28
+ - lib/bundler/overrule/dependency_rewriter.rb
29
+ - lib/bundler/overrule/dsl.rb
30
+ - lib/bundler/overrule/errors.rb
31
+ - lib/bundler/overrule/patcher.rb
32
+ - lib/bundler/overrule/registry.rb
33
+ - lib/bundler/overrule/reporter.rb
34
+ - lib/bundler/overrule/rule.rb
35
+ - lib/bundler/overrule/version.rb
36
+ - plugins.rb
37
+ homepage: https://github.com/TheSoloHacker47/bundler-overrule
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ homepage_uri: https://github.com/TheSoloHacker47/bundler-overrule
42
+ source_code_uri: https://github.com/TheSoloHacker47/bundler-overrule
43
+ changelog_uri: https://github.com/TheSoloHacker47/bundler-overrule/blob/main/CHANGELOG.md
44
+ bug_tracker_uri: https://github.com/TheSoloHacker47/bundler-overrule/issues
45
+ documentation_uri: https://github.com/TheSoloHacker47/bundler-overrule#readme
46
+ rubygems_mfa_required: 'true'
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '3.1'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.6.9
62
+ specification_version: 4
63
+ summary: Force, ban, and swap gem versions in your Gemfile — the Bundler overrides
64
+ Cargo and npm already have.
65
+ test_files: []