Skip to content

Commit 4db4b92

Browse files
authored
Merge pull request #47 from appraisal-rb/parallelize-appraisal-commands
Parallelize remaining appraisal commands
2 parents f82fe6f + 1509c01 commit 4db4b92

13 files changed

Lines changed: 591 additions & 161 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ Please file a bug if you notice a violation of semantic versioning.
2020

2121
### Added
2222

23-
- `appraisal generate` can now generate appraisal Gemfiles in parallel with
24-
`--jobs` / `-j`, or `APPRAISAL_JOBS`.
23+
- `appraisal generate`, `install`, `update`, `generate-install`, and
24+
`generate-update` can now process appraisals in parallel with
25+
`--appraisal-jobs` / `-n`, or `APPRAISAL_JOBS`. Appraisal-level
26+
parallelism defaults to 2 workers; use `-n 1` to run serially. Bundler older
27+
than 2.1 falls back to serial processing.
2528

2629
### Changed
2730

@@ -41,6 +44,17 @@ Please file a bug if you notice a violation of semantic versioning.
4144

4245
### Fixed
4346

47+
- Appraisal command execution now checks whether the `bundle` executable can
48+
boot before falling back to RubyGems spec probing and Bundler installation,
49+
preserving TruffleRuby's engine-shipped Bundler in isolated subprocesses.
50+
- Acceptance fixture Bundler selection now detects TruffleRuby's shipped
51+
Bundler without reusing a newer Bundler activated by the appraised suite.
52+
- Acceptance fixture binstubs now pin the selected Bundler version before
53+
loading generated `bin/bundle` handoff code.
54+
- Bundler-backed appraisal installs now set `BUNDLE_JOBS` explicitly so the
55+
configured Appraisal job count also controls Bundler's installer worker
56+
count in subprocesses.
57+
4458
### Security
4559

4660
## [3.1.4] - 2026-07-01

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,4 @@ DEPENDENCIES
443443
yard-yaml (~> 0.2, >= 0.2.3)
444444

445445
BUNDLED WITH
446-
4.0.14
446+
4.0.16

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -259,10 +259,20 @@ Use the command that matches the lifecycle you want:
259259
| `generate-install` | Yes | Yes, via install | First setup, or after intentional Appraisals changes |
260260
| `generate-update` | Yes | Yes, via update | Regenerate gemfiles, then update dependency locks |
261261

262-
`generate`, `generate-install`, and `generate-update` can generate appraisal
263-
gemfiles in parallel with `--jobs` / `-j`, or with `APPRAISAL_JOBS` when no
264-
CLI value is provided. `generate-install` and `generate-update` parallelize the
265-
generation phase, then resolve dependencies with the selected gem manager.
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.
266276

267277
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.
268278

@@ -275,12 +285,27 @@ Built-in dependency commands support the following options:
275285
| Option | Description |
276286
|--------|-------------|
277287
| `--gem-manager`, `-g` | Gem manager to use: `bundler` (default) or `ore`; applies to `install`, `update`, `generate-install`, and `generate-update` |
278-
| `--jobs`, `-j` | Generate appraisal gemfiles in parallel for `generate`, `generate-install`, and `generate-update`; also installs gems in parallel for `install` and `generate-install` when supported by the selected gem manager |
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` |
279290
| `--retry` | Retry network and git requests that have failed; applies to `install` and `generate-install` (default: 1) |
280291
| `--without` | A space-separated list of groups to skip during installation; applies to `install` and `generate-install` |
281292
| `--full-index` | Run bundle install with the full-index argument; applies to `install` and `generate-install` |
282293
| `--path` | Install gems in the specified directory; applies to `install` and `generate-install` |
283294

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+
284309
### Using Commands with Named Appraisals
285310

286311
#### Using Appraisal's built-in commands with named appraisals

lib/appraisal/appraisal.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def update(gems = [], options = {})
9494
end
9595

9696
def gemfile_path
97-
gemfile_root.mkdir unless gemfile_root.exist?
97+
FileUtils.mkdir_p(gemfile_root)
9898

9999
gemfile_root.join(gemfile_name).to_s
100100
end

lib/appraisal/cli.rb

Lines changed: 60 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,23 @@
66

77
module Appraisal
88
class CLI < Thor
9+
MINIMUM_PARALLEL_BUNDLER_VERSION = Gem::Version.new("2.1.0")
10+
911
default_task :install
1012
map ["-v", "--version"] => "version"
13+
map "generate-install" => "generate_install"
14+
map "generate-update" => "generate_update"
1115

1216
class_option "gem-manager",
1317
:aliases => "-g",
1418
:type => :string,
1519
:default => "bundler",
1620
: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."
1726

1827
class << self
1928
# Override help command to print out usage
@@ -60,7 +69,7 @@ def strip_heredoc(string)
6069
:type => :numeric,
6170
:default => 1,
6271
:banner => "SIZE",
63-
:desc => "Install gems in parallel using the given number of workers."
72+
:desc => "Pass the given parallel job count to the selected gem manager."
6473
method_option "retry",
6574
:type => :numeric,
6675
:default => 1,
@@ -79,7 +88,7 @@ def strip_heredoc(string)
7988
"Bundler will remember this option."
8089

8190
def install
82-
install_appraisals(appraisals, install_options)
91+
install_appraisals(appraisals, install_options, appraisal_jobs(options))
8392
end
8493

8594
desc "generate-install", "Generate gemfiles, then resolve and install dependencies for each appraisal"
@@ -88,7 +97,7 @@ def install
8897
:type => :numeric,
8998
:default => 1,
9099
:banner => "SIZE",
91-
:desc => "Install gems in parallel using the given number of workers."
100+
:desc => "Pass the given parallel job count to the selected gem manager."
92101
method_option "retry",
93102
:type => :numeric,
94103
:default => 1,
@@ -107,18 +116,13 @@ def install
107116
"Bundler will remember this option."
108117
def generate_install
109118
appraisal_list = appraisals
110-
generate_appraisals(appraisal_list, generate_jobs(options))
111-
install_appraisals(appraisal_list, install_options)
119+
generate_appraisals(appraisal_list, appraisal_jobs(options))
120+
install_appraisals(appraisal_list, install_options, appraisal_jobs(options))
112121
end
113122

114123
desc "generate", "Generate a gemfile for each appraisal"
115-
method_option "jobs",
116-
:aliases => "j",
117-
:type => :numeric,
118-
:banner => "SIZE",
119-
:desc => "Generate appraisal gemfiles in parallel using the given number of workers."
120124
def generate
121-
generate_appraisals(appraisals, generate_jobs(options))
125+
generate_appraisals(appraisals, appraisal_jobs(options))
122126
end
123127

124128
desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder"
@@ -128,19 +132,14 @@ def clean
128132

129133
desc "update [LIST_OF_GEMS]", "Update dependencies for each generated appraisal gemfile"
130134
def update(*gems)
131-
update_appraisals(appraisals, gems, update_options)
135+
update_appraisals(appraisals, gems, update_options, appraisal_jobs(options))
132136
end
133137

134138
desc "generate-update [LIST_OF_GEMS]", "Generate gemfiles, then update dependencies for each appraisal"
135-
method_option "jobs",
136-
:aliases => "j",
137-
:type => :numeric,
138-
:banner => "SIZE",
139-
:desc => "Generate appraisal gemfiles in parallel using the given number of workers."
140139
def generate_update(*gems)
141140
appraisal_list = appraisals
142-
generate_appraisals(appraisal_list, generate_jobs(options))
143-
update_appraisals(appraisal_list, gems, update_options)
141+
generate_appraisals(appraisal_list, appraisal_jobs(options))
142+
update_appraisals(appraisal_list, gems, update_options, appraisal_jobs(options))
144143
end
145144

146145
desc "list", "List the names of the defined appraisals"
@@ -165,15 +164,15 @@ def generate_appraisals(appraisal_list, jobs)
165164
end
166165
end
167166

168-
def install_appraisals(appraisal_list, install_options)
169-
appraisal_list.each do |appraisal|
167+
def install_appraisals(appraisal_list, install_options, jobs)
168+
each_appraisal(appraisal_list, jobs) do |appraisal|
170169
appraisal.install(install_options.dup)
171170
appraisal.relativize
172171
end
173172
end
174173

175-
def update_appraisals(appraisal_list, gems, update_options)
176-
appraisal_list.each do |appraisal|
174+
def update_appraisals(appraisal_list, gems, update_options, jobs)
175+
each_appraisal(appraisal_list, jobs) do |appraisal|
177176
appraisal.update(gems, update_options.dup)
178177
end
179178
end
@@ -210,17 +209,29 @@ def each_appraisal(appraisal_list, jobs)
210209
def worker_count(jobs, item_count)
211210
jobs = jobs.to_i
212211
jobs = 1 if jobs < 1
212+
jobs = 1 unless parallel_appraisals_supported?
213213
return jobs if item_count.zero?
214214

215215
[jobs, item_count].min
216216
end
217217

218-
def generate_jobs(command_options)
219-
command_options["jobs"] || command_options[:jobs] || ENV["APPRAISAL_JOBS"] || 1
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
220231
end
221232

222233
def install_options
223-
options.to_h
234+
options.to_h.reject { |key, _value| key.to_s == "appraisal-jobs" }
224235
end
225236

226237
def update_options
@@ -229,6 +240,13 @@ def update_options
229240
end
230241

231242
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+
232250
matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal|
233251
appraisal.name == name.to_s
234252
end
@@ -289,7 +307,7 @@ def method_missing(name, *args)
289307
Command.new(actual_args, :gemfile => matching_appraisal.gemfile_path).run
290308
end
291309
else
292-
AppraisalFile.each do |appraisal|
310+
each_appraisal(AppraisalFile.new.appraisals, appraisal_jobs(options)) do |appraisal|
293311
Command.new(ARGV, :gemfile => appraisal.gemfile_path).run
294312
end
295313
end
@@ -316,6 +334,17 @@ def parse_external_options(args)
316334
options[:jobs] = Regexp.last_match(1).to_i
317335
when /^-j(\d+)$/
318336
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
319348
when /^--retry=(\d+)$/
320349
options[:retry] = Regexp.last_match(1).to_i
321350
when /^--without=(.+)$/
@@ -347,6 +376,10 @@ def extract_gems_and_options(args)
347376
# Next arg should be the value
348377
options[:gem_manager] = args[index + 1]
349378
skip_next = true
379+
when /^--appraisal-jobs$/
380+
skip_next = true
381+
when /^-n$/
382+
skip_next = true
350383
when /^-/
351384
# Other options are not gems, but we don't handle them all here
352385
# For now, just skip them to be safe if they start with -

0 commit comments

Comments
 (0)