toys-core 0.12.2 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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