appraisal2 3.1.4 → 3.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +50 -1
- data/README.md +32 -2
- data/lib/appraisal/appraisal.rb +1 -1
- data/lib/appraisal/cli.rb +129 -20
- data/lib/appraisal/command.rb +125 -78
- data/lib/appraisal/gem_manager/bundler_adapter.rb +15 -2
- data/lib/appraisal/utils.rb +4 -0
- data/lib/appraisal2/version.rb +1 -1
- data.tar.gz.sig +0 -0
- metadata +21 -38
- metadata.gz.sig +0 -0
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -275
- data/FUNDING.md +0 -70
- data/MIT.md +0 -21
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/certs/pboling.pem +0 -27
- /data/sig/{appraisal2/version.rbs → appraisal2.rbs} +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3ea29429e7d6b15deb35e182590258883e32ef6fecfe015c4d39d932fd4bcf96
|
|
4
|
+
data.tar.gz: 11bafabfd834fd35254e0777feaa1180be8c6f08b6ea89e92e3615cfefc2b392
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3289da2711ffa8539b1a34aba8346209e077d3fd86cf1bdb1b726bea7093bc8ae5e2f4bc62c8aa68e03783266f6c77bdb743a2dc550a4fb14fb3744714943957
|
|
7
|
+
data.tar.gz: bf38ac3a50e473895fb994b5e0e2abbcc437e18b1ec9a2f46acb232bfefce945eddbc5201c7cb1ef2ac739ed556ff3e1b1c1da739c32b3525f23bfff01c58a98
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,53 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [3.2.0] - 2026-07-18
|
|
34
|
+
|
|
35
|
+
- TAG: [v3.2.0][3.2.0t]
|
|
36
|
+
- COVERAGE: 91.00% -- 910/1000 lines in 29 files
|
|
37
|
+
- BRANCH COVERAGE: 77.54% -- 183/236 branches in 29 files
|
|
38
|
+
- 42.33% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- `appraisal generate`, `install`, `update`, `generate-install`, and
|
|
43
|
+
`generate-update` can now process appraisals in parallel with
|
|
44
|
+
`--appraisal-jobs` / `-n`, or `APPRAISAL_JOBS`. Appraisal-level
|
|
45
|
+
parallelism defaults to 2 workers; use `-n 1` to run serially. Bundler older
|
|
46
|
+
than 2.1 falls back to serial processing.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- Full dependency updates now run `bundle update --all` to avoid Bundler's
|
|
51
|
+
deprecated implicit full-update form.
|
|
52
|
+
- Acceptance specs now opt into expensive dummy-gem and bundle fixture setup
|
|
53
|
+
through metadata, and reuse a per-worker cached default fixture stage.
|
|
54
|
+
- Generated CI workflows now cache `.rspec_status` and pass it to
|
|
55
|
+
`kettle-test` so `turbo_tests2` can balance workers with historical example
|
|
56
|
+
timings.
|
|
57
|
+
- `generate-install` and `generate-update` now reuse the parsed Appraisals set
|
|
58
|
+
for their generate and install/update phases.
|
|
59
|
+
|
|
60
|
+
- kettle-jem-template-20260716-001 - Shim gemspec manifests now include
|
|
61
|
+
`LICENSE.md` instead of nonexistent `LICENSE.txt`.
|
|
62
|
+
- kettle-jem-template-20260716-002 - Generated gemspec manifests now ship fewer
|
|
63
|
+
repository-only files by default to reduce downstream distro packaging churn.
|
|
64
|
+
|
|
65
|
+
### Fixed
|
|
66
|
+
|
|
67
|
+
- Appraisal command execution now checks whether the `bundle` executable can
|
|
68
|
+
boot before falling back to RubyGems spec probing and Bundler installation,
|
|
69
|
+
preserving TruffleRuby's engine-shipped Bundler in isolated subprocesses.
|
|
70
|
+
- Acceptance fixture Bundler selection now detects TruffleRuby's shipped
|
|
71
|
+
Bundler without reusing a newer Bundler activated by the appraised suite.
|
|
72
|
+
- Acceptance fixture binstubs now pin the selected Bundler version before
|
|
73
|
+
loading generated `bin/bundle` handoff code.
|
|
74
|
+
- Bundler-backed appraisal installs now set `BUNDLE_JOBS` explicitly so the
|
|
75
|
+
configured Appraisal job count also controls Bundler's installer worker
|
|
76
|
+
count in subprocesses.
|
|
77
|
+
- JRuby and TruffleRuby workflows now correctly skip ordinary pull request
|
|
78
|
+
branches unless the branch targets `jruby/`, `truffleruby/`, or `engines/`.
|
|
79
|
+
|
|
33
80
|
## [3.1.4] - 2026-07-01
|
|
34
81
|
|
|
35
82
|
- TAG: [v3.1.4][3.1.4t]
|
|
@@ -429,7 +476,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
429
476
|
- code coverage tracked with Coveralls, QLTY.sh, and the kettle-soup-cover gem
|
|
430
477
|
- other minor fixes and improvements
|
|
431
478
|
|
|
432
|
-
[Unreleased]: https://github.com/appraisal-rb/appraisal2/compare/v3.
|
|
479
|
+
[Unreleased]: https://github.com/appraisal-rb/appraisal2/compare/v3.2.0...HEAD
|
|
480
|
+
[3.2.0]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.4...v3.2.0
|
|
481
|
+
[3.2.0t]: https://github.com/appraisal-rb/appraisal2/releases/tag/v3.2.0
|
|
433
482
|
[3.1.4]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.3...v3.1.4
|
|
434
483
|
[3.1.4t]: https://github.com/appraisal-rb/appraisal2/releases/tag/v3.1.4
|
|
435
484
|
[3.1.3]: https://github.com/appraisal-rb/appraisal2/compare/v3.1.2...v3.1.3
|
data/README.md
CHANGED
|
@@ -259,6 +259,21 @@ Use the command that matches the lifecycle you want:
|
|
|
259
259
|
| `generate-install` | Yes | Yes, via install | First setup, or after intentional Appraisals changes |
|
|
260
260
|
| `generate-update` | Yes | Yes, via update | Regenerate gemfiles, then update dependency locks |
|
|
261
261
|
|
|
262
|
+
Appraisal2 can process appraisals in parallel with `--appraisal-jobs` / `-n`,
|
|
263
|
+
or with `APPRAISAL_JOBS` when no CLI value is provided. `generate`,
|
|
264
|
+
`generate-install`, and `generate-update` use this for generation. `install`,
|
|
265
|
+
`update`, `generate-install`, and `generate-update` use it to fan out dependency
|
|
266
|
+
resolution across appraisals. External commands run across all appraisals, such
|
|
267
|
+
as `bundle exec appraisal rake test`, use `APPRAISAL_JOBS`. Appraisal-level
|
|
268
|
+
parallelism defaults to 2 workers; use `-n 1` to run serially. When Appraisal2
|
|
269
|
+
is running under Bundler older than 2.1, Appraisal2 falls back to serial
|
|
270
|
+
processing because those Bundler versions do not provide the modern environment
|
|
271
|
+
helpers used to isolate parallel Bundler subprocesses.
|
|
272
|
+
|
|
273
|
+
Install commands keep `--jobs` / `-j` for the selected gem manager. With
|
|
274
|
+
Bundler, `-j` controls `bundle install --jobs`, while `-n` controls how many
|
|
275
|
+
appraisal gemfiles Appraisal2 processes at the same time.
|
|
276
|
+
|
|
262
277
|
The deprecated rake task `rake appraisal:install` now delegates to `appraisal generate-install`, preserving its historical generate-and-install behavior while the CLI commands remain explicit.
|
|
263
278
|
|
|
264
279
|
### Command Options
|
|
@@ -270,12 +285,27 @@ Built-in dependency commands support the following options:
|
|
|
270
285
|
| Option | Description |
|
|
271
286
|
|--------|-------------|
|
|
272
287
|
| `--gem-manager`, `-g` | Gem manager to use: `bundler` (default) or `ore`; applies to `install`, `update`, `generate-install`, and `generate-update` |
|
|
273
|
-
| `--jobs`, `-
|
|
288
|
+
| `--appraisal-jobs`, `-n` | Process appraisals in parallel for `generate`, `install`, `update`, `generate-install`, and `generate-update` (default: 2; use `-n 1` for serial execution; Bundler < 2.1 falls back to serial processing) |
|
|
289
|
+
| `--jobs`, `-j` | Pass a parallel job count to the selected gem manager; applies to `install` and `generate-install` |
|
|
274
290
|
| `--retry` | Retry network and git requests that have failed; applies to `install` and `generate-install` (default: 1) |
|
|
275
291
|
| `--without` | A space-separated list of groups to skip during installation; applies to `install` and `generate-install` |
|
|
276
292
|
| `--full-index` | Run bundle install with the full-index argument; applies to `install` and `generate-install` |
|
|
277
293
|
| `--path` | Install gems in the specified directory; applies to `install` and `generate-install` |
|
|
278
294
|
|
|
295
|
+
```bash
|
|
296
|
+
# Process two appraisals at a time, using the default Appraisal worker count
|
|
297
|
+
bundle exec appraisal generate-install
|
|
298
|
+
|
|
299
|
+
# Process four appraisals at a time
|
|
300
|
+
bundle exec appraisal generate-install -n 4
|
|
301
|
+
|
|
302
|
+
# Process two appraisals at a time, and give each Bundler install four jobs
|
|
303
|
+
bundle exec appraisal generate-install -n 2 -j 4
|
|
304
|
+
|
|
305
|
+
# Run Appraisal serially when parallel appraisal processing is not desired
|
|
306
|
+
bundle exec appraisal generate-install -n 1
|
|
307
|
+
```
|
|
308
|
+
|
|
279
309
|
### Using Commands with Named Appraisals
|
|
280
310
|
|
|
281
311
|
#### Using Appraisal's built-in commands with named appraisals
|
|
@@ -752,7 +782,7 @@ Thanks for RTFM. ☺️
|
|
|
752
782
|
[📌gitmoji]: https://gitmoji.dev
|
|
753
783
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
754
784
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
755
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-
|
|
785
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-1.000-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
756
786
|
[🔐security]: https://github.com/appraisal-rb/appraisal2/blob/main/SECURITY.md
|
|
757
787
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
758
788
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
data/lib/appraisal/appraisal.rb
CHANGED
data/lib/appraisal/cli.rb
CHANGED
|
@@ -2,17 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "fileutils"
|
|
5
|
+
require "thread"
|
|
5
6
|
|
|
6
7
|
module Appraisal
|
|
7
8
|
class CLI < Thor
|
|
9
|
+
MINIMUM_PARALLEL_BUNDLER_VERSION = Gem::Version.new("2.1.0")
|
|
10
|
+
|
|
8
11
|
default_task :install
|
|
9
12
|
map ["-v", "--version"] => "version"
|
|
13
|
+
map "generate-install" => "generate_install"
|
|
14
|
+
map "generate-update" => "generate_update"
|
|
10
15
|
|
|
11
16
|
class_option "gem-manager",
|
|
12
17
|
:aliases => "-g",
|
|
13
18
|
:type => :string,
|
|
14
19
|
:default => "bundler",
|
|
15
20
|
:desc => "Gem manager to use for install/update (bundler or ore)"
|
|
21
|
+
class_option "appraisal-jobs",
|
|
22
|
+
:aliases => "-n",
|
|
23
|
+
:type => :numeric,
|
|
24
|
+
:banner => "SIZE",
|
|
25
|
+
:desc => "Process appraisals in parallel using the given number of workers."
|
|
16
26
|
|
|
17
27
|
class << self
|
|
18
28
|
# Override help command to print out usage
|
|
@@ -59,7 +69,7 @@ module Appraisal
|
|
|
59
69
|
:type => :numeric,
|
|
60
70
|
:default => 1,
|
|
61
71
|
:banner => "SIZE",
|
|
62
|
-
:desc => "
|
|
72
|
+
:desc => "Pass the given parallel job count to the selected gem manager."
|
|
63
73
|
method_option "retry",
|
|
64
74
|
:type => :numeric,
|
|
65
75
|
:default => 1,
|
|
@@ -78,11 +88,7 @@ module Appraisal
|
|
|
78
88
|
"Bundler will remember this option."
|
|
79
89
|
|
|
80
90
|
def install
|
|
81
|
-
install_options
|
|
82
|
-
AppraisalFile.each do |appraisal|
|
|
83
|
-
appraisal.install(install_options)
|
|
84
|
-
appraisal.relativize
|
|
85
|
-
end
|
|
91
|
+
install_appraisals(appraisals, install_options, appraisal_jobs(options))
|
|
86
92
|
end
|
|
87
93
|
|
|
88
94
|
desc "generate-install", "Generate gemfiles, then resolve and install dependencies for each appraisal"
|
|
@@ -91,7 +97,7 @@ module Appraisal
|
|
|
91
97
|
:type => :numeric,
|
|
92
98
|
:default => 1,
|
|
93
99
|
:banner => "SIZE",
|
|
94
|
-
:desc => "
|
|
100
|
+
:desc => "Pass the given parallel job count to the selected gem manager."
|
|
95
101
|
method_option "retry",
|
|
96
102
|
:type => :numeric,
|
|
97
103
|
:default => 1,
|
|
@@ -109,15 +115,14 @@ module Appraisal
|
|
|
109
115
|
:desc => "Install gems in the specified directory. " \
|
|
110
116
|
"Bundler will remember this option."
|
|
111
117
|
def generate_install
|
|
112
|
-
|
|
113
|
-
|
|
118
|
+
appraisal_list = appraisals
|
|
119
|
+
generate_appraisals(appraisal_list, appraisal_jobs(options))
|
|
120
|
+
install_appraisals(appraisal_list, install_options, appraisal_jobs(options))
|
|
114
121
|
end
|
|
115
122
|
|
|
116
123
|
desc "generate", "Generate a gemfile for each appraisal"
|
|
117
124
|
def generate
|
|
118
|
-
|
|
119
|
-
appraisal.write_gemfile
|
|
120
|
-
end
|
|
125
|
+
generate_appraisals(appraisals, appraisal_jobs(options))
|
|
121
126
|
end
|
|
122
127
|
|
|
123
128
|
desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder"
|
|
@@ -127,17 +132,14 @@ module Appraisal
|
|
|
127
132
|
|
|
128
133
|
desc "update [LIST_OF_GEMS]", "Update dependencies for each generated appraisal gemfile"
|
|
129
134
|
def update(*gems)
|
|
130
|
-
|
|
131
|
-
update_options = gem_manager ? {:gem_manager => gem_manager} : {}
|
|
132
|
-
AppraisalFile.each do |appraisal|
|
|
133
|
-
appraisal.update(gems, update_options)
|
|
134
|
-
end
|
|
135
|
+
update_appraisals(appraisals, gems, update_options, appraisal_jobs(options))
|
|
135
136
|
end
|
|
136
137
|
|
|
137
138
|
desc "generate-update [LIST_OF_GEMS]", "Generate gemfiles, then update dependencies for each appraisal"
|
|
138
139
|
def generate_update(*gems)
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
appraisal_list = appraisals
|
|
141
|
+
generate_appraisals(appraisal_list, appraisal_jobs(options))
|
|
142
|
+
update_appraisals(appraisal_list, gems, update_options, appraisal_jobs(options))
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
desc "list", "List the names of the defined appraisals"
|
|
@@ -152,7 +154,99 @@ module Appraisal
|
|
|
152
154
|
|
|
153
155
|
private
|
|
154
156
|
|
|
157
|
+
def appraisals
|
|
158
|
+
AppraisalFile.new.appraisals
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def generate_appraisals(appraisal_list, jobs)
|
|
162
|
+
each_appraisal(appraisal_list, jobs) do |appraisal|
|
|
163
|
+
appraisal.write_gemfile
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def install_appraisals(appraisal_list, install_options, jobs)
|
|
168
|
+
each_appraisal(appraisal_list, jobs) do |appraisal|
|
|
169
|
+
appraisal.install(install_options.dup)
|
|
170
|
+
appraisal.relativize
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def update_appraisals(appraisal_list, gems, update_options, jobs)
|
|
175
|
+
each_appraisal(appraisal_list, jobs) do |appraisal|
|
|
176
|
+
appraisal.update(gems, update_options.dup)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def each_appraisal(appraisal_list, jobs)
|
|
181
|
+
jobs = worker_count(jobs, appraisal_list.length)
|
|
182
|
+
return appraisal_list.each { |appraisal| yield(appraisal) } if jobs <= 1
|
|
183
|
+
|
|
184
|
+
queue = Queue.new
|
|
185
|
+
errors = []
|
|
186
|
+
mutex = Mutex.new
|
|
187
|
+
|
|
188
|
+
appraisal_list.each { |appraisal| queue << appraisal }
|
|
189
|
+
jobs.times { queue << nil }
|
|
190
|
+
|
|
191
|
+
# rubocop:disable ThreadSafety/NewThread
|
|
192
|
+
threads = Array.new(jobs) do
|
|
193
|
+
Thread.new do
|
|
194
|
+
while (appraisal = queue.pop)
|
|
195
|
+
begin
|
|
196
|
+
yield(appraisal)
|
|
197
|
+
rescue Exception => error # rubocop:disable Lint/RescueException
|
|
198
|
+
mutex.synchronize { errors << error }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
# rubocop:enable ThreadSafety/NewThread
|
|
204
|
+
threads.each(&:join)
|
|
205
|
+
|
|
206
|
+
raise errors.first unless errors.empty?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def worker_count(jobs, item_count)
|
|
210
|
+
jobs = jobs.to_i
|
|
211
|
+
jobs = 1 if jobs < 1
|
|
212
|
+
jobs = 1 unless parallel_appraisals_supported?
|
|
213
|
+
return jobs if item_count.zero?
|
|
214
|
+
|
|
215
|
+
[jobs, item_count].min
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def parallel_appraisals_supported?
|
|
219
|
+
# Parallel install/update workers depend on Appraisal::Command being able
|
|
220
|
+
# to build independent Bundler subprocess environments. Bundler 2.1 is the
|
|
221
|
+
# floor for the modern original_env/unbundled_env API split that replaced
|
|
222
|
+
# clean_env, so older Bundler versions keep the historical serial path.
|
|
223
|
+
bundler_version = Gem::Specification.find_all_by_name("bundler").map(&:version).max
|
|
224
|
+
return false unless bundler_version
|
|
225
|
+
|
|
226
|
+
bundler_version >= MINIMUM_PARALLEL_BUNDLER_VERSION
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def appraisal_jobs(command_options)
|
|
230
|
+
command_options["appraisal-jobs"] || command_options[:appraisal_jobs] || ENV["APPRAISAL_JOBS"] || 2
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def install_options
|
|
234
|
+
options.to_h.reject { |key, _value| key.to_s == "appraisal-jobs" }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def update_options
|
|
238
|
+
gem_manager = options["gem-manager"] || options[:gem_manager]
|
|
239
|
+
gem_manager ? {:gem_manager => gem_manager} : {}
|
|
240
|
+
end
|
|
241
|
+
|
|
155
242
|
def method_missing(name, *args)
|
|
243
|
+
case name.to_s
|
|
244
|
+
when "generate-install"
|
|
245
|
+
return generate_install
|
|
246
|
+
when "generate-update"
|
|
247
|
+
return generate_update(*args)
|
|
248
|
+
end
|
|
249
|
+
|
|
156
250
|
matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal|
|
|
157
251
|
appraisal.name == name.to_s
|
|
158
252
|
end
|
|
@@ -213,7 +307,7 @@ module Appraisal
|
|
|
213
307
|
Command.new(actual_args, :gemfile => matching_appraisal.gemfile_path).run
|
|
214
308
|
end
|
|
215
309
|
else
|
|
216
|
-
AppraisalFile.
|
|
310
|
+
each_appraisal(AppraisalFile.new.appraisals, appraisal_jobs(options)) do |appraisal|
|
|
217
311
|
Command.new(ARGV, :gemfile => appraisal.gemfile_path).run
|
|
218
312
|
end
|
|
219
313
|
end
|
|
@@ -240,6 +334,17 @@ module Appraisal
|
|
|
240
334
|
options[:jobs] = Regexp.last_match(1).to_i
|
|
241
335
|
when /^-j(\d+)$/
|
|
242
336
|
options[:jobs] = Regexp.last_match(1).to_i
|
|
337
|
+
when /^-j$/
|
|
338
|
+
options[:jobs] = args[index + 1].to_i
|
|
339
|
+
skip_next = true
|
|
340
|
+
when /^--appraisal-jobs=\d+$/
|
|
341
|
+
nil
|
|
342
|
+
when /^--appraisal-jobs$/
|
|
343
|
+
skip_next = true
|
|
344
|
+
when /^-n\d+$/
|
|
345
|
+
nil
|
|
346
|
+
when /^-n$/
|
|
347
|
+
skip_next = true
|
|
243
348
|
when /^--retry=(\d+)$/
|
|
244
349
|
options[:retry] = Regexp.last_match(1).to_i
|
|
245
350
|
when /^--without=(.+)$/
|
|
@@ -271,6 +376,10 @@ module Appraisal
|
|
|
271
376
|
# Next arg should be the value
|
|
272
377
|
options[:gem_manager] = args[index + 1]
|
|
273
378
|
skip_next = true
|
|
379
|
+
when /^--appraisal-jobs$/
|
|
380
|
+
skip_next = true
|
|
381
|
+
when /^-n$/
|
|
382
|
+
skip_next = true
|
|
274
383
|
when /^-/
|
|
275
384
|
# Other options are not gems, but we don't handle them all here
|
|
276
385
|
# For now, just skip them to be safe if they start with -
|
data/lib/appraisal/command.rb
CHANGED
|
@@ -8,7 +8,9 @@ module Appraisal
|
|
|
8
8
|
attr_reader :command, :env, :gemfile
|
|
9
9
|
|
|
10
10
|
# BUNDLE_* environment variables that must be preserved for proper bundler operation
|
|
11
|
-
# and test isolation. These are preserved
|
|
11
|
+
# and test isolation. These are preserved after starting from Bundler's
|
|
12
|
+
# unbundled environment so Appraisal can run a fresh Bundler subprocess
|
|
13
|
+
# without losing the target appraisal and isolation settings:
|
|
12
14
|
# - Bundler version switching works (BUNDLE_GEMFILE)
|
|
13
15
|
# - Test isolation is maintained (BUNDLE_APP_CONFIG, etc.)
|
|
14
16
|
# - User settings are respected (BUNDLE_PATH, BUNDLE_USER_CACHE, etc.)
|
|
@@ -30,6 +32,7 @@ module Appraisal
|
|
|
30
32
|
|
|
31
33
|
PRESERVED_RUNTIME_VARS = [
|
|
32
34
|
"PATH",
|
|
35
|
+
"GEM_HOME",
|
|
33
36
|
"GEM_PATH"
|
|
34
37
|
].freeze
|
|
35
38
|
|
|
@@ -44,101 +47,112 @@ module Appraisal
|
|
|
44
47
|
run_env = test_environment.merge(env)
|
|
45
48
|
|
|
46
49
|
if @skip_bundle_exec
|
|
47
|
-
execute(run_env)
|
|
50
|
+
execute(ENV.to_h, run_env)
|
|
48
51
|
else
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
# Solution: Use a selective environment approach instead of with_original_env,
|
|
55
|
-
# which strips all BUNDLE_* variables and breaks version switching.
|
|
56
|
-
with_bundler_env do
|
|
57
|
-
apply_run_env(run_env)
|
|
58
|
-
ensure_bundler_is_available
|
|
59
|
-
ensure_locked_bundler_is_available
|
|
60
|
-
execute(run_env)
|
|
61
|
-
end
|
|
52
|
+
clean_env = bundler_env(run_env)
|
|
53
|
+
ensure_bundler_is_available(clean_env)
|
|
54
|
+
ensure_locked_bundler_is_available(clean_env)
|
|
55
|
+
execute(clean_env, run_env)
|
|
62
56
|
end
|
|
63
57
|
end
|
|
64
58
|
|
|
65
59
|
private
|
|
66
60
|
|
|
67
|
-
#
|
|
68
|
-
# inputs and test isolation settings.
|
|
61
|
+
# Build an Appraisal subprocess environment from Bundler's explicit env APIs.
|
|
69
62
|
#
|
|
70
|
-
#
|
|
71
|
-
#
|
|
72
|
-
#
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
rubyopt.reject! { |opt| opt == "-rbundler/setup" || opt.include?("bundler/setup") }
|
|
96
|
-
clean_env["RUBYOPT"] = rubyopt.join(" ") unless rubyopt.empty?
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Remove Bundler activation markers from the subprocess environment.
|
|
100
|
-
# BUNDLE_BIN_PATH pins the already-active Bundler executable, so keeping
|
|
101
|
-
# it would bypass the BUNDLED WITH version selected below.
|
|
102
|
-
clean_env.delete("BUNDLE_BIN_PATH")
|
|
103
|
-
clean_env.delete("BUNDLER_SETUP")
|
|
104
|
-
clean_env.delete("BUNDLER_VERSION")
|
|
105
|
-
|
|
106
|
-
ENV.replace(clean_env)
|
|
107
|
-
|
|
108
|
-
yield
|
|
109
|
-
ensure
|
|
110
|
-
ENV.replace(backup_env)
|
|
63
|
+
# Bundler.clean_env / Bundler.with_clean_env are removed. Bundler now exposes
|
|
64
|
+
# three related but distinct choices:
|
|
65
|
+
#
|
|
66
|
+
# - Bundler.original_env: the environment before the current Bundler activated.
|
|
67
|
+
# This is too broad for Appraisal because unrelated original BUNDLE_* values
|
|
68
|
+
# can leak into the appraisal subprocess.
|
|
69
|
+
# - Bundler.unbundled_env: original_env with Bundler activation removed. This
|
|
70
|
+
# is the right base because it removes parent Bundler state, RUBYOPT
|
|
71
|
+
# bundler/setup activation, and arbitrary BUNDLE_* values.
|
|
72
|
+
# - Bundler.with_unbundled_env: the block form of unbundled_env. Appraisal
|
|
73
|
+
# needs a Hash to pass to Kernel.system and must re-add selected variables,
|
|
74
|
+
# so the block helper is the wrong shape even though its semantics are close.
|
|
75
|
+
#
|
|
76
|
+
# Starting from unbundled_env would normally remove BUNDLE_GEMFILE,
|
|
77
|
+
# BUNDLE_APP_CONFIG, and related isolation knobs. Appraisal intentionally
|
|
78
|
+
# reintroduces only the variables it owns or must preserve, then sets
|
|
79
|
+
# BUNDLE_GEMFILE and the selected Bundler version for the target subprocess.
|
|
80
|
+
def bundler_env(run_env)
|
|
81
|
+
current_env = ENV.to_h
|
|
82
|
+
clean_env = if defined?(Bundler) && Bundler.respond_to?(:unbundled_env)
|
|
83
|
+
Bundler.unbundled_env.to_h
|
|
84
|
+
elsif defined?(Bundler) && Bundler.respond_to?(:original_env)
|
|
85
|
+
Bundler.original_env.to_h
|
|
86
|
+
else
|
|
87
|
+
current_env.to_h
|
|
111
88
|
end
|
|
112
|
-
end
|
|
113
89
|
|
|
114
|
-
|
|
115
|
-
|
|
90
|
+
# Avoid leaking a global BUNDLE_LOCKFILE into subprocesses.
|
|
91
|
+
clean_env["BUNDLE_LOCKFILE"] = nil
|
|
92
|
+
|
|
93
|
+
preserved_vars = PRESERVED_BUNDLE_VARS + PRESERVED_RUNTIME_VARS
|
|
116
94
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
ENV["BUNDLER_VERSION"] = bundler_version
|
|
95
|
+
preserved_vars.each do |var|
|
|
96
|
+
clean_env[var] = current_env[var] if current_env[var]
|
|
120
97
|
end
|
|
121
|
-
|
|
98
|
+
|
|
99
|
+
clean_env["RUBYOPT"] = current_env["RUBYOPT"] if current_env["RUBYOPT"]
|
|
100
|
+
|
|
101
|
+
# Remove bundler/setup from RUBYOPT so subprocess doesn't auto-load bundler.
|
|
102
|
+
# Bundler.original_env can also contain RUBYOPT, so strip from the final
|
|
103
|
+
# command environment rather than only from the active process env.
|
|
104
|
+
if clean_env["RUBYOPT"]
|
|
105
|
+
rubyopt = clean_env["RUBYOPT"].split(" ")
|
|
106
|
+
rubyopt.reject! { |opt| opt == "-rbundler/setup" || opt.include?("bundler/setup") }
|
|
107
|
+
clean_env["RUBYOPT"] = rubyopt.join(" ")
|
|
108
|
+
clean_env["RUBYOPT"] = nil if clean_env["RUBYOPT"].empty?
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Remove Bundler activation markers from the subprocess environment.
|
|
112
|
+
# BUNDLE_BIN_PATH pins the already-active Bundler executable, so keeping
|
|
113
|
+
# it would bypass the BUNDLED WITH version selected below.
|
|
114
|
+
clean_env["BUNDLE_BIN_PATH"] = nil
|
|
115
|
+
clean_env["BUNDLE_VERSION"] = nil
|
|
116
|
+
clean_env["BUNDLER_SETUP"] = nil
|
|
117
|
+
clean_env["BUNDLER_VERSION"] = nil
|
|
118
|
+
|
|
122
119
|
run_env.each_pair do |key, value|
|
|
123
|
-
|
|
120
|
+
clean_env[key] = value
|
|
124
121
|
end
|
|
125
122
|
|
|
126
|
-
|
|
123
|
+
clean_env
|
|
127
124
|
end
|
|
128
125
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
def execute(base_env, run_env)
|
|
127
|
+
announce
|
|
128
|
+
|
|
129
|
+
process_env = base_env.merge(run_env)
|
|
130
|
+
process_env["BUNDLE_GEMFILE"] = gemfile if gemfile
|
|
131
|
+
if (bundler_version = subprocess_bundler_version(process_env))
|
|
132
|
+
process_env["BUNDLE_VERSION"] = bundler_version
|
|
133
|
+
process_env["BUNDLER_VERSION"] = bundler_version
|
|
132
134
|
end
|
|
135
|
+
process_env["APPRAISAL_INITIALIZED"] = "1"
|
|
136
|
+
|
|
137
|
+
exit(1) unless Kernel.system(process_env, command_as_string)
|
|
133
138
|
end
|
|
134
139
|
|
|
135
|
-
def ensure_bundler_is_available
|
|
136
|
-
|
|
137
|
-
|
|
140
|
+
def ensure_bundler_is_available(process_env)
|
|
141
|
+
rubygems_env = rubygems_command_env(process_env)
|
|
142
|
+
|
|
143
|
+
# First ask the actual Bundler executable whether it can boot in the
|
|
144
|
+
# environment Appraisal is about to use. In CI, especially on alternate
|
|
145
|
+
# Ruby engines, Bundler may be available through the active app bundle
|
|
146
|
+
# while a manually scrubbed RubyGems spec lookup cannot see it through the
|
|
147
|
+
# current GEM_HOME/GEM_PATH.
|
|
148
|
+
return if system(process_env, "bundle -v > /dev/null 2>&1")
|
|
149
|
+
|
|
150
|
+
# Check if any version of bundler is available through RubyGems.
|
|
151
|
+
return if system(rubygems_env, bundler_available_command)
|
|
138
152
|
|
|
139
153
|
puts ">> Bundler not found, attempting to install..."
|
|
140
154
|
# If that fails, try to install the latest stable version
|
|
141
|
-
return if system("gem install bundler")
|
|
155
|
+
return if system(rubygems_env, "ruby --disable=gems -S gem install bundler")
|
|
142
156
|
|
|
143
157
|
puts
|
|
144
158
|
puts <<-ERROR.rstrip
|
|
@@ -150,14 +164,16 @@ manually.
|
|
|
150
164
|
exit(1)
|
|
151
165
|
end
|
|
152
166
|
|
|
153
|
-
def ensure_locked_bundler_is_available
|
|
167
|
+
def ensure_locked_bundler_is_available(process_env)
|
|
154
168
|
locked_version = locked_bundler_version
|
|
155
169
|
return unless locked_version
|
|
156
170
|
|
|
157
|
-
|
|
171
|
+
rubygems_env = rubygems_command_env(process_env)
|
|
172
|
+
|
|
173
|
+
return if system(rubygems_env, bundler_available_command(locked_version))
|
|
158
174
|
|
|
159
175
|
puts ">> Bundler #{locked_version} not found, attempting to install..."
|
|
160
|
-
return if system("gem install bundler -v #{Shellwords.escape(locked_version)} --no-document")
|
|
176
|
+
return if system(rubygems_env, "ruby --disable=gems -S gem install bundler -v #{Shellwords.escape(locked_version)} --no-document")
|
|
161
177
|
|
|
162
178
|
puts
|
|
163
179
|
puts <<-ERROR.rstrip
|
|
@@ -180,6 +196,37 @@ manually.
|
|
|
180
196
|
match && match[1]
|
|
181
197
|
end
|
|
182
198
|
|
|
199
|
+
def subprocess_bundler_version(process_env)
|
|
200
|
+
# Production subprocesses pin Bundler only when the appraisal lockfile
|
|
201
|
+
# says to. Acceptance specs may provide APPRAISAL_TEST_BUNDLER_VERSION so
|
|
202
|
+
# the nested subprocesses use the same isolated Bundler selected by the
|
|
203
|
+
# harness, without changing production behavior.
|
|
204
|
+
locked_bundler_version || process_env["APPRAISAL_TEST_BUNDLER_VERSION"]
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def rubygems_command_env(process_env)
|
|
208
|
+
process_env.dup.tap do |env|
|
|
209
|
+
env["BUNDLE_APP_CONFIG"] = nil
|
|
210
|
+
env["BUNDLE_BIN_PATH"] = nil
|
|
211
|
+
env["BUNDLE_GEMFILE"] = nil
|
|
212
|
+
env["BUNDLE_LOCKFILE"] = nil
|
|
213
|
+
env["BUNDLE_VERSION"] = nil
|
|
214
|
+
env["BUNDLER_SETUP"] = nil
|
|
215
|
+
env["BUNDLER_VERSION"] = nil
|
|
216
|
+
env["RUBYOPT"] = nil
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def bundler_available_command(version = nil)
|
|
221
|
+
requirement = version ? ", Gem::Requirement.new(#{version.inspect})" : ""
|
|
222
|
+
code = <<-RUBY.chomp
|
|
223
|
+
require "rubygems"
|
|
224
|
+
specs = Gem::Specification.find_all_by_name("bundler"#{requirement})
|
|
225
|
+
exit(specs.empty? ? 1 : 0)
|
|
226
|
+
RUBY
|
|
227
|
+
"ruby --disable=gems -e #{Shellwords.escape(code)}"
|
|
228
|
+
end
|
|
229
|
+
|
|
183
230
|
def announce
|
|
184
231
|
if gemfile
|
|
185
232
|
puts ">> BUNDLE_GEMFILE=#{gemfile} #{command_as_string}"
|
|
@@ -219,7 +266,7 @@ manually.
|
|
|
219
266
|
|
|
220
267
|
{
|
|
221
268
|
"GEM_HOME" => ENV["GEM_HOME"],
|
|
222
|
-
"GEM_PATH" => ""
|
|
269
|
+
"GEM_PATH" => ENV["APPRAISAL_TEST_SYSTEM_GEM_PATH"].to_s
|
|
223
270
|
}
|
|
224
271
|
end
|
|
225
272
|
end
|