appraisal2 3.1.4 → 3.2.1

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.
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
@@ -27,7 +37,8 @@ module Appraisal
27
37
  appraisal, otherwise it runs the EXTERNAL_COMMAND against all appraisals.
28
38
  HELP
29
39
 
30
- if File.exist?("Appraisals")
40
+ appraisals_file = ENV["APPRAISAL_FILE"] || "Appraisals"
41
+ if File.exist?(appraisals_file)
31
42
  shell.say
32
43
  shell.say("Available Appraisal(s):")
33
44
 
@@ -59,7 +70,7 @@ module Appraisal
59
70
  :type => :numeric,
60
71
  :default => 1,
61
72
  :banner => "SIZE",
62
- :desc => "Install gems in parallel using the given number of workers."
73
+ :desc => "Pass the given parallel job count to the selected gem manager."
63
74
  method_option "retry",
64
75
  :type => :numeric,
65
76
  :default => 1,
@@ -78,11 +89,7 @@ module Appraisal
78
89
  "Bundler will remember this option."
79
90
 
80
91
  def install
81
- install_options = options.to_h
82
- AppraisalFile.each do |appraisal|
83
- appraisal.install(install_options)
84
- appraisal.relativize
85
- end
92
+ install_appraisals(appraisals, install_options, appraisal_jobs(options))
86
93
  end
87
94
 
88
95
  desc "generate-install", "Generate gemfiles, then resolve and install dependencies for each appraisal"
@@ -91,7 +98,7 @@ module Appraisal
91
98
  :type => :numeric,
92
99
  :default => 1,
93
100
  :banner => "SIZE",
94
- :desc => "Install gems in parallel using the given number of workers."
101
+ :desc => "Pass the given parallel job count to the selected gem manager."
95
102
  method_option "retry",
96
103
  :type => :numeric,
97
104
  :default => 1,
@@ -109,15 +116,14 @@ module Appraisal
109
116
  :desc => "Install gems in the specified directory. " \
110
117
  "Bundler will remember this option."
111
118
  def generate_install
112
- invoke(:generate, [], {})
113
- invoke(:install, [], options.to_h)
119
+ appraisal_list = appraisals
120
+ generate_appraisals(appraisal_list, appraisal_jobs(options))
121
+ install_appraisals(appraisal_list, install_options, appraisal_jobs(options))
114
122
  end
115
123
 
116
124
  desc "generate", "Generate a gemfile for each appraisal"
117
125
  def generate
118
- AppraisalFile.each do |appraisal|
119
- appraisal.write_gemfile
120
- end
126
+ generate_appraisals(appraisals, appraisal_jobs(options))
121
127
  end
122
128
 
123
129
  desc "clean", "Remove all generated gemfiles and lockfiles from gemfiles folder"
@@ -127,17 +133,14 @@ module Appraisal
127
133
 
128
134
  desc "update [LIST_OF_GEMS]", "Update dependencies for each generated appraisal gemfile"
129
135
  def update(*gems)
130
- gem_manager = options["gem-manager"] || options[:gem_manager]
131
- update_options = gem_manager ? {:gem_manager => gem_manager} : {}
132
- AppraisalFile.each do |appraisal|
133
- appraisal.update(gems, update_options)
134
- end
136
+ update_appraisals(appraisals, gems, update_options, appraisal_jobs(options))
135
137
  end
136
138
 
137
139
  desc "generate-update [LIST_OF_GEMS]", "Generate gemfiles, then update dependencies for each appraisal"
138
140
  def generate_update(*gems)
139
- invoke(:generate, [], {})
140
- update(*gems)
141
+ appraisal_list = appraisals
142
+ generate_appraisals(appraisal_list, appraisal_jobs(options))
143
+ update_appraisals(appraisal_list, gems, update_options, appraisal_jobs(options))
141
144
  end
142
145
 
143
146
  desc "list", "List the names of the defined appraisals"
@@ -152,7 +155,99 @@ module Appraisal
152
155
 
153
156
  private
154
157
 
158
+ def appraisals
159
+ AppraisalFile.new.appraisals
160
+ end
161
+
162
+ def generate_appraisals(appraisal_list, jobs)
163
+ each_appraisal(appraisal_list, jobs) do |appraisal|
164
+ appraisal.write_gemfile
165
+ end
166
+ end
167
+
168
+ def install_appraisals(appraisal_list, install_options, jobs)
169
+ each_appraisal(appraisal_list, jobs) do |appraisal|
170
+ appraisal.install(install_options.dup)
171
+ appraisal.relativize
172
+ end
173
+ end
174
+
175
+ def update_appraisals(appraisal_list, gems, update_options, jobs)
176
+ each_appraisal(appraisal_list, jobs) do |appraisal|
177
+ appraisal.update(gems, update_options.dup)
178
+ end
179
+ end
180
+
181
+ def each_appraisal(appraisal_list, jobs)
182
+ jobs = worker_count(jobs, appraisal_list.length)
183
+ return appraisal_list.each { |appraisal| yield(appraisal) } if jobs <= 1
184
+
185
+ queue = Queue.new
186
+ errors = []
187
+ mutex = Mutex.new
188
+
189
+ appraisal_list.each { |appraisal| queue << appraisal }
190
+ jobs.times { queue << nil }
191
+
192
+ # rubocop:disable ThreadSafety/NewThread
193
+ threads = Array.new(jobs) do
194
+ Thread.new do
195
+ while (appraisal = queue.pop)
196
+ begin
197
+ yield(appraisal)
198
+ rescue Exception => error # rubocop:disable Lint/RescueException
199
+ mutex.synchronize { errors << error }
200
+ end
201
+ end
202
+ end
203
+ end
204
+ # rubocop:enable ThreadSafety/NewThread
205
+ threads.each(&:join)
206
+
207
+ raise errors.first unless errors.empty?
208
+ end
209
+
210
+ def worker_count(jobs, item_count)
211
+ jobs = jobs.to_i
212
+ jobs = 1 if jobs < 1
213
+ jobs = 1 unless parallel_appraisals_supported?
214
+ return jobs if item_count.zero?
215
+
216
+ [jobs, item_count].min
217
+ end
218
+
219
+ def parallel_appraisals_supported?
220
+ # Parallel install/update workers depend on Appraisal::Command being able
221
+ # to build independent Bundler subprocess environments. Bundler 2.1 is the
222
+ # floor for the modern original_env/unbundled_env API split that replaced
223
+ # clean_env, so older Bundler versions keep the historical serial path.
224
+ bundler_version = Gem::Specification.find_all_by_name("bundler").map(&:version).max
225
+ return false unless bundler_version
226
+
227
+ bundler_version >= MINIMUM_PARALLEL_BUNDLER_VERSION
228
+ end
229
+
230
+ def appraisal_jobs(command_options)
231
+ command_options["appraisal-jobs"] || command_options[:appraisal_jobs] || ENV["APPRAISAL_JOBS"] || 2
232
+ end
233
+
234
+ def install_options
235
+ options.to_h.reject { |key, _value| key.to_s == "appraisal-jobs" }
236
+ end
237
+
238
+ def update_options
239
+ gem_manager = options["gem-manager"] || options[:gem_manager]
240
+ gem_manager ? {:gem_manager => gem_manager} : {}
241
+ end
242
+
155
243
  def method_missing(name, *args)
244
+ case name.to_s
245
+ when "generate-install"
246
+ return generate_install
247
+ when "generate-update"
248
+ return generate_update(*args)
249
+ end
250
+
156
251
  matching_appraisal = AppraisalFile.new.appraisals.detect do |appraisal|
157
252
  appraisal.name == name.to_s
158
253
  end
@@ -213,7 +308,7 @@ module Appraisal
213
308
  Command.new(actual_args, :gemfile => matching_appraisal.gemfile_path).run
214
309
  end
215
310
  else
216
- AppraisalFile.each do |appraisal|
311
+ each_appraisal(AppraisalFile.new.appraisals, appraisal_jobs(options)) do |appraisal|
217
312
  Command.new(ARGV, :gemfile => appraisal.gemfile_path).run
218
313
  end
219
314
  end
@@ -240,6 +335,17 @@ module Appraisal
240
335
  options[:jobs] = Regexp.last_match(1).to_i
241
336
  when /^-j(\d+)$/
242
337
  options[:jobs] = Regexp.last_match(1).to_i
338
+ when /^-j$/
339
+ options[:jobs] = args[index + 1].to_i
340
+ skip_next = true
341
+ when /^--appraisal-jobs=\d+$/
342
+ nil
343
+ when /^--appraisal-jobs$/
344
+ skip_next = true
345
+ when /^-n\d+$/
346
+ nil
347
+ when /^-n$/
348
+ skip_next = true
243
349
  when /^--retry=(\d+)$/
244
350
  options[:retry] = Regexp.last_match(1).to_i
245
351
  when /^--without=(.+)$/
@@ -271,6 +377,10 @@ module Appraisal
271
377
  # Next arg should be the value
272
378
  options[:gem_manager] = args[index + 1]
273
379
  skip_next = true
380
+ when /^--appraisal-jobs$/
381
+ skip_next = true
382
+ when /^-n$/
383
+ skip_next = true
274
384
  when /^-/
275
385
  # Other options are not gems, but we don't handle them all here
276
386
  # For now, just skip them to be safe if they start with -
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rbconfig"
3
4
  require "shellwords"
4
5
 
5
6
  module Appraisal
@@ -8,7 +9,9 @@ module Appraisal
8
9
  attr_reader :command, :env, :gemfile
9
10
 
10
11
  # BUNDLE_* environment variables that must be preserved for proper bundler operation
11
- # and test isolation. These are preserved when using with_bundler_env to ensure:
12
+ # and test isolation. These are preserved after starting from Bundler's
13
+ # unbundled environment so Appraisal can run a fresh Bundler subprocess
14
+ # without losing the target appraisal and isolation settings:
12
15
  # - Bundler version switching works (BUNDLE_GEMFILE)
13
16
  # - Test isolation is maintained (BUNDLE_APP_CONFIG, etc.)
14
17
  # - User settings are respected (BUNDLE_PATH, BUNDLE_USER_CACHE, etc.)
@@ -30,6 +33,7 @@ module Appraisal
30
33
 
31
34
  PRESERVED_RUNTIME_VARS = [
32
35
  "PATH",
36
+ "GEM_HOME",
33
37
  "GEM_PATH"
34
38
  ].freeze
35
39
 
@@ -44,101 +48,115 @@ module Appraisal
44
48
  run_env = test_environment.merge(env)
45
49
 
46
50
  if @skip_bundle_exec
47
- execute(run_env)
51
+ execute(ENV.to_h, run_env)
48
52
  else
49
- # For bundler version switching to work reliably, we need to preserve
50
- # the appraisal Gemfile while avoiding the active parent Bundler process.
51
- # However, we still need to isolate from the parent's bundler state
52
- # to avoid conflicts.
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
53
+ clean_env = bundler_env(run_env)
54
+ ensure_bundler_is_available(clean_env)
55
+ ensure_locked_bundler_is_available(clean_env)
56
+ execute(clean_env, run_env)
62
57
  end
63
58
  end
64
59
 
65
60
  private
66
61
 
67
- # Provide a clean environment while preserving Bundler's version switching
68
- # inputs and test isolation settings.
62
+ # Build an Appraisal subprocess environment from Bundler's explicit env APIs.
69
63
  #
70
- # The current Ruby process has bundler activated, which adds bundler/setup to RUBYOPT.
71
- # When we run a subprocess, we need to remove that activation so the subprocess bundler
72
- # can start fresh. However, we keep all test isolation variables intact.
73
- def with_bundler_env
74
- backup_env = ENV.to_h
75
-
76
- begin
77
- clean_env = if Bundler.respond_to?(:original_env)
78
- Bundler.original_env.to_h
79
- else
80
- backup_env.to_h
81
- end
82
-
83
- # Avoid leaking a global BUNDLE_LOCKFILE into subprocesses.
84
- clean_env.delete("BUNDLE_LOCKFILE")
85
-
86
- preserved_vars = PRESERVED_BUNDLE_VARS + PRESERVED_RUNTIME_VARS
87
-
88
- preserved_vars.each do |var|
89
- clean_env[var] = backup_env[var] if backup_env[var]
90
- end
91
-
92
- # Remove bundler/setup from RUBYOPT so subprocess doesn't auto-load bundler
93
- if backup_env["RUBYOPT"]
94
- rubyopt = backup_env["RUBYOPT"].split(" ")
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)
64
+ # Bundler.clean_env / Bundler.with_clean_env are removed. Bundler now exposes
65
+ # three related but distinct choices:
66
+ #
67
+ # - Bundler.original_env: the environment before the current Bundler activated.
68
+ # This is too broad for Appraisal because unrelated original BUNDLE_* values
69
+ # can leak into the appraisal subprocess.
70
+ # - Bundler.unbundled_env: original_env with Bundler activation removed. This
71
+ # is the right base because it removes parent Bundler state, RUBYOPT
72
+ # bundler/setup activation, and arbitrary BUNDLE_* values.
73
+ # - Bundler.with_unbundled_env: the block form of unbundled_env. Appraisal
74
+ # needs a Hash to pass to Kernel.system and must re-add selected variables,
75
+ # so the block helper is the wrong shape even though its semantics are close.
76
+ #
77
+ # Starting from unbundled_env would normally remove BUNDLE_GEMFILE,
78
+ # BUNDLE_APP_CONFIG, and related isolation knobs. Appraisal intentionally
79
+ # reintroduces only the variables it owns or must preserve, then sets
80
+ # BUNDLE_GEMFILE and the selected Bundler version for the target subprocess.
81
+ def bundler_env(run_env)
82
+ current_env = ENV.to_h
83
+ clean_env = if defined?(Bundler) && Bundler.respond_to?(:unbundled_env)
84
+ Bundler.unbundled_env.to_h
85
+ elsif defined?(Bundler) && Bundler.respond_to?(:original_env)
86
+ Bundler.original_env.to_h
87
+ else
88
+ current_env.to_h
111
89
  end
112
- end
113
90
 
114
- def execute(run_env)
115
- announce
91
+ # Avoid leaking a global BUNDLE_LOCKFILE into subprocesses.
92
+ clean_env["BUNDLE_LOCKFILE"] = nil
93
+
94
+ preserved_vars = PRESERVED_BUNDLE_VARS + PRESERVED_RUNTIME_VARS
95
+
96
+ preserved_vars.each do |var|
97
+ clean_env[var] = current_env[var] if current_env[var]
98
+ end
99
+
100
+ clean_env["RUBYOPT"] = current_env["RUBYOPT"] if current_env["RUBYOPT"]
116
101
 
117
- ENV["BUNDLE_GEMFILE"] = gemfile
118
- if (bundler_version = locked_bundler_version)
119
- ENV["BUNDLER_VERSION"] = bundler_version
102
+ # Remove bundler/setup from RUBYOPT so subprocess doesn't auto-load bundler.
103
+ # Bundler.original_env can also contain RUBYOPT, so strip from the final
104
+ # command environment rather than only from the active process env.
105
+ if clean_env["RUBYOPT"]
106
+ rubyopt = clean_env["RUBYOPT"].split(" ")
107
+ rubyopt.reject! { |opt| opt == "-rbundler/setup" || opt.include?("bundler/setup") }
108
+ clean_env["RUBYOPT"] = rubyopt.join(" ")
109
+ clean_env["RUBYOPT"] = nil if clean_env["RUBYOPT"].empty?
120
110
  end
121
- ENV["APPRAISAL_INITIALIZED"] = "1"
111
+
112
+ # Remove Bundler activation markers from the subprocess environment.
113
+ # BUNDLE_BIN_PATH pins the already-active Bundler executable, so keeping
114
+ # it would bypass the BUNDLED WITH version selected below.
115
+ clean_env["BUNDLE_BIN_PATH"] = nil
116
+ clean_env["BUNDLE_VERSION"] = nil
117
+ clean_env["BUNDLER_SETUP"] = nil
118
+ clean_env["BUNDLER_VERSION"] = nil
119
+
122
120
  run_env.each_pair do |key, value|
123
- ENV[key] = value
121
+ clean_env[key] = value
124
122
  end
125
123
 
126
- exit(1) unless Kernel.system(command_as_string)
124
+ clean_env["RUBYLIB"] = nil
125
+
126
+ clean_env
127
127
  end
128
128
 
129
- def apply_run_env(run_env)
130
- run_env.each_pair do |key, value|
131
- ENV[key] = value
129
+ def execute(base_env, run_env)
130
+ process_env = base_env.merge(run_env)
131
+ process_env["BUNDLE_GEMFILE"] = gemfile if gemfile
132
+ bundler_version = subprocess_bundler_version(process_env)
133
+ announce
134
+ command_text = command_as_string(:bundler_version => bundler_version)
135
+ if bundler_version
136
+ process_env["BUNDLE_VERSION"] = bundler_version
137
+ process_env["BUNDLER_VERSION"] = bundler_version
132
138
  end
139
+ process_env["APPRAISAL_INITIALIZED"] = "1"
140
+
141
+ exit(1) unless Kernel.system(process_env, command_text)
133
142
  end
134
143
 
135
- def ensure_bundler_is_available
136
- # Check if any version of bundler is available
137
- return if system(%(gem list --silent -i bundler))
144
+ def ensure_bundler_is_available(process_env)
145
+ rubygems_env = rubygems_command_env(process_env)
146
+
147
+ # First ask the actual Bundler executable whether it can boot in the
148
+ # environment Appraisal is about to use. In CI, especially on alternate
149
+ # Ruby engines, Bundler may be available through the active app bundle
150
+ # while a manually scrubbed RubyGems spec lookup cannot see it through the
151
+ # current GEM_HOME/GEM_PATH.
152
+ return if system(process_env, "bundle -v > /dev/null 2>&1")
153
+
154
+ # Check if any version of bundler is available through RubyGems.
155
+ return if system(rubygems_env, bundler_available_command)
138
156
 
139
157
  puts ">> Bundler not found, attempting to install..."
140
158
  # If that fails, try to install the latest stable version
141
- return if system("gem install bundler")
159
+ return if system(rubygems_env, "ruby --disable=gems -S gem install bundler")
142
160
 
143
161
  puts
144
162
  puts <<-ERROR.rstrip
@@ -150,14 +168,16 @@ manually.
150
168
  exit(1)
151
169
  end
152
170
 
153
- def ensure_locked_bundler_is_available
171
+ def ensure_locked_bundler_is_available(process_env)
154
172
  locked_version = locked_bundler_version
155
173
  return unless locked_version
156
174
 
157
- return if system(%(gem list --silent -i bundler -v "#{locked_version}"))
175
+ rubygems_env = rubygems_command_env(process_env)
176
+
177
+ return if system(rubygems_env, bundler_available_command(locked_version))
158
178
 
159
179
  puts ">> Bundler #{locked_version} not found, attempting to install..."
160
- return if system("gem install bundler -v #{Shellwords.escape(locked_version)} --no-document")
180
+ return if system(rubygems_env, "ruby --disable=gems -S gem install bundler -v #{Shellwords.escape(locked_version)} --no-document")
161
181
 
162
182
  puts
163
183
  puts <<-ERROR.rstrip
@@ -180,11 +200,42 @@ manually.
180
200
  match && match[1]
181
201
  end
182
202
 
183
- def announce
203
+ def subprocess_bundler_version(process_env)
204
+ # Production subprocesses pin Bundler only when the appraisal lockfile
205
+ # says to. Acceptance specs may provide APPRAISAL_TEST_BUNDLER_VERSION so
206
+ # the nested subprocesses use the same isolated Bundler selected by the
207
+ # harness, without changing production behavior.
208
+ locked_bundler_version || process_env["APPRAISAL_TEST_BUNDLER_VERSION"]
209
+ end
210
+
211
+ def rubygems_command_env(process_env)
212
+ process_env.dup.tap do |env|
213
+ env["BUNDLE_APP_CONFIG"] = nil
214
+ env["BUNDLE_BIN_PATH"] = nil
215
+ env["BUNDLE_GEMFILE"] = nil
216
+ env["BUNDLE_LOCKFILE"] = nil
217
+ env["BUNDLE_VERSION"] = nil
218
+ env["BUNDLER_SETUP"] = nil
219
+ env["BUNDLER_VERSION"] = nil
220
+ env["RUBYOPT"] = nil
221
+ end
222
+ end
223
+
224
+ def bundler_available_command(version = nil)
225
+ requirement = version ? ", Gem::Requirement.new(#{version.inspect})" : ""
226
+ code = <<-RUBY.chomp
227
+ require "rubygems"
228
+ specs = Gem::Specification.find_all_by_name("bundler"#{requirement})
229
+ exit(specs.empty? ? 1 : 0)
230
+ RUBY
231
+ "ruby --disable=gems -e #{Shellwords.escape(code)}"
232
+ end
233
+
234
+ def announce(command_text = command_as_string)
184
235
  if gemfile
185
- puts ">> BUNDLE_GEMFILE=#{gemfile} #{command_as_string}"
236
+ puts ">> BUNDLE_GEMFILE=#{gemfile} #{command_text}"
186
237
  else
187
- puts ">> #{command_as_string}"
238
+ puts ">> #{command_text}"
188
239
  end
189
240
  end
190
241
 
@@ -206,12 +257,20 @@ manually.
206
257
  end
207
258
  end
208
259
 
209
- def command_as_string
210
- if command.is_a?(Array)
260
+ def command_as_string(bundler_version: nil)
261
+ command_text = if command.is_a?(Array)
211
262
  Shellwords.join(command)
212
263
  else
213
264
  command
214
265
  end
266
+ return command_text unless bundler_version
267
+
268
+ command_text.gsub(/\bbundle(?=\s)/) { versioned_bundler_command(bundler_version) }
269
+ end
270
+
271
+ def versioned_bundler_command(version)
272
+ script = %(gem "bundler", #{version.inspect}; load Gem.bin_path("bundler", "bundle"))
273
+ Shellwords.join([RbConfig.ruby, "-e", script])
215
274
  end
216
275
 
217
276
  def test_environment
@@ -219,7 +278,7 @@ manually.
219
278
 
220
279
  {
221
280
  "GEM_HOME" => ENV["GEM_HOME"],
222
- "GEM_PATH" => ""
281
+ "GEM_PATH" => ENV["APPRAISAL_TEST_SYSTEM_GEM_PATH"].to_s
223
282
  }
224
283
  end
225
284
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "shellwords"
4
4
 
5
+ require "appraisal/utils"
6
+
5
7
  require_relative "base"
6
8
 
7
9
  module Appraisal
@@ -53,7 +55,16 @@ module Appraisal
53
55
  end
54
56
 
55
57
  def update_command(gems)
56
- ["bundle", "update", *gems].compact
58
+ gems = Array(gems).compact
59
+ return full_update_command if gems.empty?
60
+
61
+ ["bundle", "update", *gems]
62
+ end
63
+
64
+ def full_update_command
65
+ return ["bundle", "update", "--all"] if Utils.support_bundle_update_all?
66
+
67
+ ["bundle", "update"]
57
68
  end
58
69
 
59
70
  def bundle_options(options)
@@ -85,12 +96,14 @@ module Appraisal
85
96
  end
86
97
 
87
98
  def install_environment(options)
88
- env = {}
99
+ env = {"BUNDLE_JOBS" => DEFAULT_INSTALL_OPTIONS.fetch("jobs").to_s}
89
100
 
90
101
  if options["path"].nil? && Bundler.settings[:path]
91
102
  env["BUNDLE_DISABLE_SHARED_GEMS"] = "1"
92
103
  end
93
104
 
105
+ env["BUNDLE_JOBS"] = options["jobs"].to_s if options["jobs"].to_i > 1 && Utils.support_parallel_installation?
106
+
94
107
  env
95
108
  end
96
109
  end
@@ -12,6 +12,10 @@ module Appraisal
12
12
  Gem::Version.create(Bundler::VERSION) > Gem::Version.create("2.4.22")
13
13
  end
14
14
 
15
+ def support_bundle_update_all?
16
+ Gem::Version.create(Bundler::VERSION) >= Gem::Version.create("1.16.5")
17
+ end
18
+
15
19
  # Appraisal needs to print Gemfiles in the oldest Ruby syntax that is supported by Appraisal.
16
20
  # Otherwise, a project would not be able to use Appraisal to test compatibility
17
21
  # with older versions of Ruby, which is a core use case for Appraisal.
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appraisal
4
+ # Version namespace for this gem.
4
5
  module Version
5
- VERSION = "3.1.4"
6
+ # Current gem version.
7
+ VERSION = "3.2.1"
6
8
  end
7
- VERSION = Version::VERSION # Traditional constant location
9
+ # Current gem version exposed at the traditional constant location.
10
+ VERSION = Version::VERSION # Traditional Constant Location
8
11
  end
data/lib/appraisal.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "appraisal/version"
4
+
3
5
  # simplecov:disable
4
6
  require "appraisal2"
5
7
  # simplecov:enable
data/lib/appraisal2.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "appraisal2/version"
4
-
5
3
  require "appraisal/version"
6
4
  require "appraisal/task"
7
5
 
data.tar.gz.sig CHANGED
Binary file