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 +7 -0
- data/CHANGELOG.md +48 -0
- data/LICENSE +21 -0
- data/README.md +205 -0
- data/lib/bundler/overrule/command.rb +95 -0
- data/lib/bundler/overrule/dependency_rewriter.rb +101 -0
- data/lib/bundler/overrule/dsl.rb +36 -0
- data/lib/bundler/overrule/errors.rb +47 -0
- data/lib/bundler/overrule/patcher.rb +144 -0
- data/lib/bundler/overrule/registry.rb +115 -0
- data/lib/bundler/overrule/reporter.rb +286 -0
- data/lib/bundler/overrule/rule.rb +111 -0
- data/lib/bundler/overrule/version.rb +7 -0
- data/lib/bundler/overrule.rb +158 -0
- data/lib/bundler-overrule.rb +32 -0
- data/plugins.rb +33 -0
- metadata +65 -0
|
@@ -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: []
|