toys-core 0.12.2 → 0.13.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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +4 -1
  5. data/docs/guide.md +1 -1
  6. data/lib/toys/acceptor.rb +10 -1
  7. data/lib/toys/arg_parser.rb +1 -0
  8. data/lib/toys/cli.rb +127 -107
  9. data/lib/toys/compat.rb +54 -3
  10. data/lib/toys/completion.rb +15 -5
  11. data/lib/toys/context.rb +22 -20
  12. data/lib/toys/core.rb +6 -2
  13. data/lib/toys/dsl/base.rb +2 -0
  14. data/lib/toys/dsl/flag.rb +23 -17
  15. data/lib/toys/dsl/flag_group.rb +11 -7
  16. data/lib/toys/dsl/positional_arg.rb +23 -13
  17. data/lib/toys/dsl/tool.rb +10 -6
  18. data/lib/toys/errors.rb +63 -8
  19. data/lib/toys/flag.rb +660 -651
  20. data/lib/toys/flag_group.rb +19 -6
  21. data/lib/toys/input_file.rb +9 -3
  22. data/lib/toys/loader.rb +129 -115
  23. data/lib/toys/middleware.rb +45 -21
  24. data/lib/toys/mixin.rb +8 -6
  25. data/lib/toys/positional_arg.rb +18 -17
  26. data/lib/toys/settings.rb +81 -67
  27. data/lib/toys/source_info.rb +33 -24
  28. data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
  29. data/lib/toys/standard_middleware/apply_config.rb +1 -0
  30. data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
  31. data/lib/toys/standard_middleware/set_default_descriptions.rb +1 -0
  32. data/lib/toys/standard_middleware/show_help.rb +2 -0
  33. data/lib/toys/standard_middleware/show_root_version.rb +2 -0
  34. data/lib/toys/standard_mixins/bundler.rb +22 -14
  35. data/lib/toys/standard_mixins/exec.rb +31 -20
  36. data/lib/toys/standard_mixins/fileutils.rb +3 -1
  37. data/lib/toys/standard_mixins/gems.rb +21 -17
  38. data/lib/toys/standard_mixins/git_cache.rb +5 -7
  39. data/lib/toys/standard_mixins/highline.rb +8 -8
  40. data/lib/toys/standard_mixins/terminal.rb +5 -5
  41. data/lib/toys/standard_mixins/xdg.rb +5 -5
  42. data/lib/toys/template.rb +9 -7
  43. data/lib/toys/tool_definition.rb +209 -202
  44. data/lib/toys/utils/completion_engine.rb +7 -2
  45. data/lib/toys/utils/exec.rb +158 -127
  46. data/lib/toys/utils/gems.rb +81 -57
  47. data/lib/toys/utils/git_cache.rb +674 -45
  48. data/lib/toys/utils/help_text.rb +27 -3
  49. data/lib/toys/utils/terminal.rb +10 -2
  50. data/lib/toys/wrappable_string.rb +9 -2
  51. data/lib/toys-core.rb +14 -5
  52. metadata +4 -4
@@ -280,7 +280,7 @@ module Toys
280
280
  #
281
281
  def exec_ruby(args, **opts, &block)
282
282
  cmd = args.is_a?(::Array) ? [::RbConfig.ruby] + args : "#{::RbConfig.ruby} #{args}"
283
- log_cmd = args.is_a?(::Array) ? ["ruby"] + args : "ruby #{args}"
283
+ log_cmd = "exec ruby: #{args.inspect}"
284
284
  opts = {argv0: "ruby", log_cmd: log_cmd}.merge(opts)
285
285
  exec(cmd, **opts, &block)
286
286
  end
@@ -396,92 +396,6 @@ module Toys
396
396
  exec(cmd, **opts, &block).exit_code
397
397
  end
398
398
 
399
- ##
400
- # An internal helper class storing the configuration of a subprocess invocation
401
- # @private
402
- #
403
- class Opts
404
- ##
405
- # Option keys that belong to exec configuration
406
- # @private
407
- #
408
- CONFIG_KEYS = [
409
- :argv0,
410
- :background,
411
- :cli,
412
- :env,
413
- :err,
414
- :in,
415
- :logger,
416
- :log_cmd,
417
- :log_level,
418
- :name,
419
- :out,
420
- :result_callback,
421
- ].freeze
422
-
423
- ##
424
- # Option keys that belong to spawn configuration
425
- # @private
426
- #
427
- SPAWN_KEYS = [
428
- :chdir,
429
- :close_others,
430
- :new_pgroup,
431
- :pgroup,
432
- :umask,
433
- :unsetenv_others,
434
- ].freeze
435
-
436
- ## @private
437
- def initialize(parent = nil)
438
- if parent
439
- @config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
440
- @spawn_opts = ::Hash.new { |_h, k| parent.spawn_opts[k] }
441
- elsif block_given?
442
- @config_opts = ::Hash.new { |_h, k| yield k }
443
- @spawn_opts = ::Hash.new { |_h, k| yield k }
444
- else
445
- @config_opts = {}
446
- @spawn_opts = {}
447
- end
448
- end
449
-
450
- ## @private
451
- def add(config)
452
- config.each do |k, v|
453
- if CONFIG_KEYS.include?(k)
454
- @config_opts[k] = v
455
- elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
456
- @spawn_opts[k] = v
457
- else
458
- raise ::ArgumentError, "Unknown key: #{k.inspect}"
459
- end
460
- end
461
- self
462
- end
463
-
464
- ## @private
465
- def delete(*keys)
466
- keys.each do |k|
467
- if CONFIG_KEYS.include?(k)
468
- @config_opts.delete(k)
469
- elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
470
- @spawn_opts.delete(k)
471
- else
472
- raise ::ArgumentError, "Unknown key: #{k.inspect}"
473
- end
474
- end
475
- self
476
- end
477
-
478
- ## @private
479
- attr_reader :config_opts
480
-
481
- ## @private
482
- attr_reader :spawn_opts
483
- end
484
-
485
399
  ##
486
400
  # An object that controls a subprocess. This object is returned from an
487
401
  # execution running in the background, or is yielded to a control block
@@ -490,28 +404,6 @@ module Toys
490
404
  # send signals to the process, and get its result.
491
405
  #
492
406
  class Controller
493
- ## @private
494
- def initialize(name, controller_streams, captures, pid, join_threads,
495
- result_callback, mutex)
496
- @name = name
497
- @in = controller_streams[:in]
498
- @out = controller_streams[:out]
499
- @err = controller_streams[:err]
500
- @captures = captures
501
- @pid = @exception = @wait_thread = nil
502
- case pid
503
- when ::Integer
504
- @pid = pid
505
- @wait_thread = ::Process.detach(pid)
506
- when ::Exception
507
- @exception = pid
508
- end
509
- @join_threads = join_threads
510
- @result_callback = result_callback
511
- @mutex = mutex
512
- @result = nil
513
- end
514
-
515
407
  ##
516
408
  # The subcommand's name.
517
409
  # @return [Object]
@@ -746,8 +638,33 @@ module Toys
746
638
  end
747
639
  end
748
640
 
641
+ ##
642
+ # @private
643
+ #
644
+ def initialize(name, controller_streams, captures, pid, join_threads,
645
+ result_callback, mutex)
646
+ @name = name
647
+ @in = controller_streams[:in]
648
+ @out = controller_streams[:out]
649
+ @err = controller_streams[:err]
650
+ @captures = captures
651
+ @pid = @exception = @wait_thread = nil
652
+ case pid
653
+ when ::Integer
654
+ @pid = pid
655
+ @wait_thread = ::Process.detach(pid)
656
+ when ::Exception
657
+ @exception = pid
658
+ end
659
+ @join_threads = join_threads
660
+ @result_callback = result_callback
661
+ @mutex = mutex
662
+ @result = nil
663
+ end
664
+
749
665
  ##
750
666
  # Close the controller's streams.
667
+ #
751
668
  # @private
752
669
  #
753
670
  def close_streams(which)
@@ -799,15 +716,6 @@ module Toys
799
716
  # return the numeric signal code.
800
717
  #
801
718
  class Result
802
- ## @private
803
- def initialize(name, out, err, status, exception)
804
- @name = name
805
- @captured_out = out
806
- @captured_err = err
807
- @status = status
808
- @exception = exception
809
- end
810
-
811
719
  ##
812
720
  # The subcommand's name.
813
721
  #
@@ -928,13 +836,129 @@ module Toys
928
836
  code = exit_code
929
837
  !code.nil? && !code.zero?
930
838
  end
839
+
840
+ ##
841
+ # @private
842
+ #
843
+ def initialize(name, out, err, status, exception)
844
+ @name = name
845
+ @captured_out = out
846
+ @captured_err = err
847
+ @status = status
848
+ @exception = exception
849
+ end
850
+ end
851
+
852
+ private
853
+
854
+ ##
855
+ # An internal helper class storing the configuration of a subprocess invocation
856
+ #
857
+ # @private
858
+ #
859
+ class Opts
860
+ ##
861
+ # Option keys that belong to exec configuration
862
+ #
863
+ # @private
864
+ #
865
+ CONFIG_KEYS = [
866
+ :argv0,
867
+ :background,
868
+ :cli,
869
+ :env,
870
+ :err,
871
+ :in,
872
+ :logger,
873
+ :log_cmd,
874
+ :log_level,
875
+ :name,
876
+ :out,
877
+ :result_callback,
878
+ ].freeze
879
+
880
+ ##
881
+ # Option keys that belong to spawn configuration
882
+ #
883
+ # @private
884
+ #
885
+ SPAWN_KEYS = [
886
+ :chdir,
887
+ :close_others,
888
+ :new_pgroup,
889
+ :pgroup,
890
+ :umask,
891
+ :unsetenv_others,
892
+ ].freeze
893
+
894
+ ##
895
+ # @private
896
+ #
897
+ def initialize(parent = nil)
898
+ if parent
899
+ @config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
900
+ @spawn_opts = ::Hash.new { |_h, k| parent.spawn_opts[k] }
901
+ elsif block_given?
902
+ @config_opts = ::Hash.new { |_h, k| yield k }
903
+ @spawn_opts = ::Hash.new { |_h, k| yield k }
904
+ else
905
+ @config_opts = {}
906
+ @spawn_opts = {}
907
+ end
908
+ end
909
+
910
+ ##
911
+ # @private
912
+ #
913
+ def add(config)
914
+ config.each do |k, v|
915
+ if CONFIG_KEYS.include?(k)
916
+ @config_opts[k] = v
917
+ elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
918
+ @spawn_opts[k] = v
919
+ else
920
+ raise ::ArgumentError, "Unknown key: #{k.inspect}"
921
+ end
922
+ end
923
+ self
924
+ end
925
+
926
+ ##
927
+ # @private
928
+ #
929
+ def delete(*keys)
930
+ keys.each do |k|
931
+ if CONFIG_KEYS.include?(k)
932
+ @config_opts.delete(k)
933
+ elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
934
+ @spawn_opts.delete(k)
935
+ else
936
+ raise ::ArgumentError, "Unknown key: #{k.inspect}"
937
+ end
938
+ end
939
+ self
940
+ end
941
+
942
+ ##
943
+ # @private
944
+ #
945
+ attr_reader :config_opts
946
+
947
+ ##
948
+ # @private
949
+ #
950
+ attr_reader :spawn_opts
931
951
  end
932
952
 
933
953
  ##
934
954
  # An object that manages the execution of a subcommand
955
+ #
935
956
  # @private
936
957
  #
937
958
  class Executor
959
+ ##
960
+ # @private
961
+ #
938
962
  def initialize(exec_opts, spawn_cmd, block)
939
963
  @fork_func = spawn_cmd.respond_to?(:call) ? spawn_cmd : nil
940
964
  @spawn_cmd = spawn_cmd.respond_to?(:call) ? nil : spawn_cmd
@@ -950,6 +974,9 @@ module Toys
950
974
  @mutex = ::Mutex.new
951
975
  end
952
976
 
977
+ ##
978
+ # @private
979
+ #
953
980
  def execute
954
981
  setup_in_stream
955
982
  setup_out_stream(:out)
@@ -970,17 +997,23 @@ module Toys
970
997
  def log_command
971
998
  logger = @config_opts[:logger]
972
999
  if logger && @config_opts[:log_level] != false
973
- cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
1000
+ cmd_str = @config_opts[:log_cmd] || default_log_str
974
1001
  logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
975
1002
  end
976
1003
  end
977
1004
 
978
- def default_log_str(spawn_cmd)
979
- return nil unless spawn_cmd
980
- return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
981
- cmd_binary = spawn_cmd.first
982
- cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
983
- ([cmd_binary] + spawn_cmd[1..-1]).inspect
1005
+ def default_log_str
1006
+ if @fork_func
1007
+ "exec proc: #{@fork_func.inspect}"
1008
+ elsif @spawn_cmd
1009
+ if @spawn_cmd.size == 1 && @spawn_cmd.first.is_a?(::String)
1010
+ "exec sh: #{@spawn_cmd.first.inspect}"
1011
+ else
1012
+ cmd_binary = @spawn_cmd.first
1013
+ cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
1014
+ "exec: #{([cmd_binary] + @spawn_cmd[1..-1]).inspect}"
1015
+ end
1016
+ end
984
1017
  end
985
1018
 
986
1019
  def start_with_controller
@@ -1311,8 +1344,6 @@ module Toys
1311
1344
  end
1312
1345
  end
1313
1346
 
1314
- private
1315
-
1316
1347
  def canonical_binary_spec(cmd, exec_opts)
1317
1348
  config_argv0 = exec_opts.config_opts[:argv0]
1318
1349
  return cmd.to_s if !config_argv0 && !cmd.is_a?(::Array)
@@ -188,15 +188,16 @@ module Toys
188
188
  gemfile_path = ::File.absolute_path(gemfile_path)
189
189
  Gems.synchronize do
190
190
  if configure_gemfile(gemfile_path)
191
- activate("bundler", "~> 2.1")
191
+ activate("bundler", "~> 2.2")
192
192
  require "bundler"
193
- lockfile_path = find_lockfile_path(gemfile_path)
194
- setup_bundle(gemfile_path, lockfile_path, groups: groups, retries: retries)
193
+ setup_bundle(gemfile_path, groups: groups, retries: retries)
195
194
  end
196
195
  end
197
196
  end
198
197
 
198
+ ##
199
199
  # @private
200
+ #
200
201
  def self.find_gemfile(search_dir, gemfile_names: nil)
201
202
  gemfile_names ||= DEFAULT_GEMFILE_NAMES
202
203
  Array(gemfile_names).each do |file|
@@ -208,7 +209,9 @@ module Toys
208
209
 
209
210
  @global_mutex = ::Monitor.new
210
211
 
212
+ ##
211
213
  # @private
214
+ #
212
215
  def self.synchronize(&block)
213
216
  @global_mutex.synchronize(&block)
214
217
  end
@@ -255,8 +258,7 @@ module Toys
255
258
  def confirm_and_install_gem(name, requirements)
256
259
  if @on_missing == :confirm
257
260
  requirements_text = gem_requirements_text(name, requirements)
258
- response = terminal.confirm("Gem needed: #{requirements_text}. Install? ",
259
- default: @default_confirm)
261
+ response = terminal.confirm("Gem needed: #{requirements_text}. Install? ", default: @default_confirm)
260
262
  unless response
261
263
  raise InstallFailedError, "Canceled installation of needed gem: #{requirements_text}"
262
264
  end
@@ -301,71 +303,92 @@ module Toys
301
303
  end
302
304
  end
303
305
 
304
- def setup_bundle(gemfile_path, lockfile_path, groups: nil, retries: nil)
306
+ def setup_bundle(gemfile_path, groups: nil, retries: nil)
307
+ check_gemfile_compatibility(gemfile_path)
305
308
  groups = Array(groups)
306
- old_lockfile_contents = save_old_lockfile(lockfile_path)
309
+ modified_gemfile_path = create_modified_gemfile(gemfile_path)
307
310
  begin
308
- modify_bundle_definition(gemfile_path, lockfile_path)
309
- ::Bundler.ui.silence { ::Bundler.setup(*groups) }
311
+ attempt_setup_bundle(modified_gemfile_path, groups)
310
312
  rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
311
- restore_toys_libs
312
- install_bundle(gemfile_path, retries: retries)
313
- old_lockfile_contents = save_old_lockfile(lockfile_path)
314
313
  ::Bundler.reset!
315
- modify_bundle_definition(gemfile_path, lockfile_path)
316
- ::Bundler.ui.silence { ::Bundler.setup(*groups) }
314
+ restore_toys_libs
315
+ install_bundle(modified_gemfile_path, retries: retries)
316
+ attempt_setup_bundle(modified_gemfile_path, groups)
317
+ ensure
318
+ delete_modified_gemfile(modified_gemfile_path)
319
+ ::ENV["BUNDLE_GEMFILE"] = gemfile_path
317
320
  end
318
321
  restore_toys_libs
319
- ensure
320
- restore_old_lockfile(lockfile_path, old_lockfile_contents)
321
322
  end
322
323
 
323
- def save_old_lockfile(lockfile_path)
324
- return nil unless ::File.readable?(lockfile_path) && ::File.writable?(lockfile_path)
325
- ::File.read(lockfile_path)
326
- end
327
-
328
- def restore_old_lockfile(lockfile_path, contents)
329
- return unless contents
330
- ::File.open(lockfile_path, "w") do |file|
331
- file.write(contents)
324
+ def attempt_setup_bundle(modified_gemfile_path, groups)
325
+ ::ENV["BUNDLE_GEMFILE"] = modified_gemfile_path
326
+ ::Bundler.configure
327
+ ::Bundler.settings.temporary({gemfile: modified_gemfile_path}) do
328
+ ::Bundler.ui.silence do
329
+ ::Bundler.setup(*groups)
330
+ end
332
331
  end
333
332
  end
334
333
 
335
- def modify_bundle_definition(gemfile_path, lockfile_path)
334
+ def check_gemfile_compatibility(gemfile_path)
336
335
  ::Bundler.configure
337
336
  builder = ::Bundler::Dsl.new
338
337
  builder.eval_gemfile(gemfile_path)
339
- toys_gems = ["toys-core"]
340
- remove_gem_from_definition(builder, "toys-core")
341
- removed_toys = remove_gem_from_definition(builder, "toys")
342
- add_gem_to_definition(builder, "toys-core")
343
- if removed_toys || ::Toys.const_defined?(:VERSION)
344
- add_gem_to_definition(builder, "toys")
345
- toys_gems << "toys"
346
- end
347
- definition = builder.to_definition(lockfile_path, { gems: toys_gems })
348
- ::Bundler.instance_variable_set(:@definition, definition)
338
+ check_gemfile_gem_compatibility(builder, "toys-core")
339
+ check_gemfile_gem_compatibility(builder, "toys")
340
+ ::Bundler.reset!
349
341
  end
350
342
 
351
- def remove_gem_from_definition(builder, name)
343
+ def check_gemfile_gem_compatibility(builder, name)
352
344
  existing_dep = builder.dependencies.find { |dep| dep.name == name }
353
- return false unless existing_dep
354
- unless existing_dep.requirement.satisfied_by?(::Gem::Version.new(::Toys::Core::VERSION))
345
+ if existing_dep && !existing_dep.requirement.satisfied_by?(::Gem::Version.new(::Toys::Core::VERSION))
355
346
  raise IncompatibleToysError,
356
347
  "The bundle lists #{name} #{existing_dep.requirement} as a dependency, which is" \
357
- " incompatible with the current version #{::Toys::Core::VERSION}."
348
+ " incompatible with the current toys version #{::Toys::Core::VERSION}."
358
349
  end
359
- builder.dependencies.delete(existing_dep)
360
- true
361
350
  end
362
351
 
363
- def add_gem_to_definition(builder, name)
364
- if ::ENV["TOYS_DEV"] == "true"
365
- path = ::File.join(::File.dirname(::File.dirname(::Toys::CORE_LIB_PATH)), name)
352
+ def create_modified_gemfile(gemfile_path)
353
+ dir = ::File.dirname(gemfile_path)
354
+ modified_gemfile_path = loop do
355
+ timestamp = ::Time.now.strftime("%Y%m%d%H%M%S")
356
+ uniquifier = rand(3_656_158_440_062_976).to_s(36) # 10 digits in base 36
357
+ path = ::File.join(dir, ".toys-tmp-gemfile-#{timestamp}-#{uniquifier}")
358
+ break path unless ::File.exist?(path)
359
+ end
360
+ ::File.open(modified_gemfile_path, "w") do |file|
361
+ modified_gemfile_content(gemfile_path).each do |line|
362
+ file.puts(line)
363
+ end
366
364
  end
367
- command = "gem #{name.inspect}, #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}\n"
368
- builder.eval_gemfile("current #{name}", command)
365
+ lockfile_path = find_lockfile_path(gemfile_path)
366
+ modified_lockfile_path = find_lockfile_path(modified_gemfile_path)
367
+ if ::File.readable?(lockfile_path)
368
+ lockfile_content = ::File.read(lockfile_path)
369
+ ::File.open(modified_lockfile_path, "w") { |file| file.write(lockfile_content) }
370
+ end
371
+ modified_gemfile_path
372
+ end
373
+
374
+ def modified_gemfile_content(gemfile_path)
375
+ is_running_toys = ::Toys.const_defined?(:VERSION)
376
+ content = [::File.read(gemfile_path)]
377
+ content << "has_toys_dep = dependencies.any? { |dep| dep.name == 'toys' }" unless is_running_toys
378
+ content << "dependencies.delete_if { |dep| dep.name == 'toys-core' || dep.name == 'toys' }"
379
+ repo_root = ::File.dirname(::File.dirname(::Toys::CORE_LIB_PATH)) if ::ENV["TOYS_DEV"]
380
+ path = repo_root ? ::File.join(repo_root, "toys-core") : nil
381
+ content << "gem 'toys-core', #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}"
382
+ path = repo_root ? ::File.join(repo_root, "toys") : nil
383
+ guard = is_running_toys ? "" : " if has_toys_dep"
384
+ content << "gem 'toys', #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}#{guard}"
385
+ content
386
+ end
387
+
388
+ def delete_modified_gemfile(modified_gemfile_path)
389
+ ::File.delete(modified_gemfile_path) if ::File.exist?(modified_gemfile_path)
390
+ modified_lockfile_path = find_lockfile_path(modified_gemfile_path)
391
+ ::File.delete(modified_lockfile_path) if ::File.exist?(modified_lockfile_path)
369
392
  end
370
393
 
371
394
  def restore_toys_libs
@@ -384,8 +407,7 @@ module Toys
384
407
  when :error
385
408
  false
386
409
  else
387
- terminal.confirm("Your bundle requires additional gems. Install? ",
388
- default: @default_confirm)
410
+ terminal.confirm("Your bundle requires additional gems. Install? ", default: @default_confirm)
389
411
  end
390
412
  end
391
413
 
@@ -393,17 +415,19 @@ module Toys
393
415
  gemfile_dir = ::File.dirname(gemfile_path)
394
416
  unless permission_to_bundle?
395
417
  raise BundleNotInstalledError,
396
- "Your bundle is not installed. Consider running" \
397
- " `cd #{gemfile_dir} && bundle install`"
418
+ "Your bundle is not installed. Consider running `cd #{gemfile_dir} && bundle install`"
398
419
  end
399
420
  retries = retries.to_i
400
- args = retries.positive? ? ["--retry=#{retries}"] : []
401
- require "bundler/cli"
402
- begin
403
- ::Bundler::CLI.start(["install"] + args)
404
- rescue ::Bundler::GemNotFound, ::Bundler::InstallError, ::Bundler::VersionConflict
421
+ args = ["--gemfile=#{gemfile_path}"]
422
+ args << "--retry=#{retries}" if retries.positive?
423
+ bundler_bin = ::Gem.bin_path("bundler", "bundle", ::Bundler::VERSION)
424
+ result = exec_util.exec_ruby([bundler_bin, "install"] + args)
425
+ if result.error?
405
426
  terminal.puts("Failed to install. Trying update...")
406
- ::Bundler::CLI.start(["update"] + args)
427
+ result = exec_util.exec_ruby([bundler_bin, "update"] + args)
428
+ unless result.success?
429
+ raise ::Bundler::InstallError, "Failed to install or update bundle: #{gemfile_path}"
430
+ end
407
431
  end
408
432
  end
409
433
  end