toys-core 0.11.5 → 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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +62 -0
  3. data/LICENSE.md +1 -1
  4. data/README.md +5 -2
  5. data/docs/guide.md +1 -1
  6. data/lib/toys/acceptor.rb +13 -4
  7. data/lib/toys/arg_parser.rb +7 -7
  8. data/lib/toys/cli.rb +170 -120
  9. data/lib/toys/compat.rb +71 -23
  10. data/lib/toys/completion.rb +18 -6
  11. data/lib/toys/context.rb +24 -15
  12. data/lib/toys/core.rb +6 -2
  13. data/lib/toys/dsl/base.rb +87 -0
  14. data/lib/toys/dsl/flag.rb +26 -20
  15. data/lib/toys/dsl/flag_group.rb +18 -14
  16. data/lib/toys/dsl/internal.rb +206 -0
  17. data/lib/toys/dsl/positional_arg.rb +26 -16
  18. data/lib/toys/dsl/tool.rb +180 -218
  19. data/lib/toys/errors.rb +64 -8
  20. data/lib/toys/flag.rb +662 -656
  21. data/lib/toys/flag_group.rb +24 -10
  22. data/lib/toys/input_file.rb +13 -7
  23. data/lib/toys/loader.rb +293 -140
  24. data/lib/toys/middleware.rb +46 -22
  25. data/lib/toys/mixin.rb +10 -8
  26. data/lib/toys/positional_arg.rb +21 -20
  27. data/lib/toys/settings.rb +914 -0
  28. data/lib/toys/source_info.rb +147 -35
  29. data/lib/toys/standard_middleware/add_verbosity_flags.rb +2 -0
  30. data/lib/toys/standard_middleware/apply_config.rb +6 -4
  31. data/lib/toys/standard_middleware/handle_usage_errors.rb +1 -0
  32. data/lib/toys/standard_middleware/set_default_descriptions.rb +19 -18
  33. data/lib/toys/standard_middleware/show_help.rb +19 -5
  34. data/lib/toys/standard_middleware/show_root_version.rb +2 -0
  35. data/lib/toys/standard_mixins/bundler.rb +24 -15
  36. data/lib/toys/standard_mixins/exec.rb +43 -34
  37. data/lib/toys/standard_mixins/fileutils.rb +3 -1
  38. data/lib/toys/standard_mixins/gems.rb +21 -17
  39. data/lib/toys/standard_mixins/git_cache.rb +46 -0
  40. data/lib/toys/standard_mixins/highline.rb +8 -8
  41. data/lib/toys/standard_mixins/terminal.rb +5 -5
  42. data/lib/toys/standard_mixins/xdg.rb +56 -0
  43. data/lib/toys/template.rb +11 -9
  44. data/lib/toys/{tool.rb → tool_definition.rb} +292 -226
  45. data/lib/toys/utils/completion_engine.rb +7 -2
  46. data/lib/toys/utils/exec.rb +162 -132
  47. data/lib/toys/utils/gems.rb +85 -60
  48. data/lib/toys/utils/git_cache.rb +813 -0
  49. data/lib/toys/utils/help_text.rb +117 -37
  50. data/lib/toys/utils/terminal.rb +11 -3
  51. data/lib/toys/utils/xdg.rb +293 -0
  52. data/lib/toys/wrappable_string.rb +9 -2
  53. data/lib/toys-core.rb +18 -6
  54. metadata +14 -7
@@ -54,6 +54,7 @@ module Toys
54
54
 
55
55
  ##
56
56
  # Internal completion method designed for testing.
57
+ #
57
58
  # @private
58
59
  #
59
60
  def run_internal(line)
@@ -78,7 +79,9 @@ module Toys
78
79
  end
79
80
 
80
81
  class << self
81
- ## @private
82
+ ##
83
+ # @private
84
+ #
82
85
  def split(line)
83
86
  words = []
84
87
  field = ::String.new
@@ -97,7 +100,9 @@ module Toys
97
100
  words
98
101
  end
99
102
 
100
- ## @private
103
+ ##
104
+ # @private
105
+ #
101
106
  def format_candidate(candidate, quote_type)
102
107
  str = candidate.to_s
103
108
  partial = candidate.is_a?(Completion::Candidate) ? candidate.partial? : false
@@ -16,8 +16,6 @@ module Toys
16
16
  # This class is not loaded by default. Before using it directly, you should
17
17
  # `require "toys/utils/exec"`
18
18
  #
19
- # ## Features
20
- #
21
19
  # ### Controlling processes
22
20
  #
23
21
  # A process can be started in the *foreground* or the *background*. If you
@@ -138,7 +136,7 @@ module Toys
138
136
  # end
139
137
  # exec_service.exec(["git", "init"], result_callback: my_callback)
140
138
  #
141
- # ## Configuration options
139
+ # ### Configuration options
142
140
  #
143
141
  # A variety of options can be used to control subprocesses. These can be
144
142
  # provided to any method that starts a subprocess. Youc an also set
@@ -282,7 +280,7 @@ module Toys
282
280
  #
283
281
  def exec_ruby(args, **opts, &block)
284
282
  cmd = args.is_a?(::Array) ? [::RbConfig.ruby] + args : "#{::RbConfig.ruby} #{args}"
285
- log_cmd = args.is_a?(::Array) ? ["ruby"] + args : "ruby #{args}"
283
+ log_cmd = "exec ruby: #{args.inspect}"
286
284
  opts = {argv0: "ruby", log_cmd: log_cmd}.merge(opts)
287
285
  exec(cmd, **opts, &block)
288
286
  end
@@ -398,92 +396,6 @@ module Toys
398
396
  exec(cmd, **opts, &block).exit_code
399
397
  end
400
398
 
401
- ##
402
- # An internal helper class storing the configuration of a subprocess invocation
403
- # @private
404
- #
405
- class Opts
406
- ##
407
- # Option keys that belong to exec configuration
408
- # @private
409
- #
410
- CONFIG_KEYS = [
411
- :argv0,
412
- :background,
413
- :cli,
414
- :env,
415
- :err,
416
- :in,
417
- :logger,
418
- :log_cmd,
419
- :log_level,
420
- :name,
421
- :out,
422
- :result_callback,
423
- ].freeze
424
-
425
- ##
426
- # Option keys that belong to spawn configuration
427
- # @private
428
- #
429
- SPAWN_KEYS = [
430
- :chdir,
431
- :close_others,
432
- :new_pgroup,
433
- :pgroup,
434
- :umask,
435
- :unsetenv_others,
436
- ].freeze
437
-
438
- ## @private
439
- def initialize(parent = nil)
440
- if parent
441
- @config_opts = ::Hash.new { |_h, k| parent.config_opts[k] }
442
- @spawn_opts = ::Hash.new { |_h, k| parent.spawn_opts[k] }
443
- elsif block_given?
444
- @config_opts = ::Hash.new { |_h, k| yield k }
445
- @spawn_opts = ::Hash.new { |_h, k| yield k }
446
- else
447
- @config_opts = {}
448
- @spawn_opts = {}
449
- end
450
- end
451
-
452
- ## @private
453
- def add(config)
454
- config.each do |k, v|
455
- if CONFIG_KEYS.include?(k)
456
- @config_opts[k] = v
457
- elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
458
- @spawn_opts[k] = v
459
- else
460
- raise ::ArgumentError, "Unknown key: #{k.inspect}"
461
- end
462
- end
463
- self
464
- end
465
-
466
- ## @private
467
- def delete(*keys)
468
- keys.each do |k|
469
- if CONFIG_KEYS.include?(k)
470
- @config_opts.delete(k)
471
- elsif SPAWN_KEYS.include?(k) || k.to_s.start_with?("rlimit_")
472
- @spawn_opts.delete(k)
473
- else
474
- raise ::ArgumentError, "Unknown key: #{k.inspect}"
475
- end
476
- end
477
- self
478
- end
479
-
480
- ## @private
481
- attr_reader :config_opts
482
-
483
- ## @private
484
- attr_reader :spawn_opts
485
- end
486
-
487
399
  ##
488
400
  # An object that controls a subprocess. This object is returned from an
489
401
  # execution running in the background, or is yielded to a control block
@@ -492,28 +404,6 @@ module Toys
492
404
  # send signals to the process, and get its result.
493
405
  #
494
406
  class Controller
495
- ## @private
496
- def initialize(name, controller_streams, captures, pid, join_threads,
497
- result_callback, mutex)
498
- @name = name
499
- @in = controller_streams[:in]
500
- @out = controller_streams[:out]
501
- @err = controller_streams[:err]
502
- @captures = captures
503
- @pid = @exception = @wait_thread = nil
504
- case pid
505
- when ::Integer
506
- @pid = pid
507
- @wait_thread = ::Process.detach(pid)
508
- when ::Exception
509
- @exception = pid
510
- end
511
- @join_threads = join_threads
512
- @result_callback = result_callback
513
- @mutex = mutex
514
- @result = nil
515
- end
516
-
517
407
  ##
518
408
  # The subcommand's name.
519
409
  # @return [Object]
@@ -748,8 +638,33 @@ module Toys
748
638
  end
749
639
  end
750
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
+
751
665
  ##
752
666
  # Close the controller's streams.
667
+ #
753
668
  # @private
754
669
  #
755
670
  def close_streams(which)
@@ -801,15 +716,6 @@ module Toys
801
716
  # return the numeric signal code.
802
717
  #
803
718
  class Result
804
- ## @private
805
- def initialize(name, out, err, status, exception)
806
- @name = name
807
- @captured_out = out
808
- @captured_err = err
809
- @status = status
810
- @exception = exception
811
- end
812
-
813
719
  ##
814
720
  # The subcommand's name.
815
721
  #
@@ -930,13 +836,129 @@ module Toys
930
836
  code = exit_code
931
837
  !code.nil? && !code.zero?
932
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
933
951
  end
934
952
 
935
953
  ##
936
954
  # An object that manages the execution of a subcommand
955
+ #
937
956
  # @private
938
957
  #
939
958
  class Executor
959
+ ##
960
+ # @private
961
+ #
940
962
  def initialize(exec_opts, spawn_cmd, block)
941
963
  @fork_func = spawn_cmd.respond_to?(:call) ? spawn_cmd : nil
942
964
  @spawn_cmd = spawn_cmd.respond_to?(:call) ? nil : spawn_cmd
@@ -952,6 +974,9 @@ module Toys
952
974
  @mutex = ::Mutex.new
953
975
  end
954
976
 
977
+ ##
978
+ # @private
979
+ #
955
980
  def execute
956
981
  setup_in_stream
957
982
  setup_out_stream(:out)
@@ -972,17 +997,23 @@ module Toys
972
997
  def log_command
973
998
  logger = @config_opts[:logger]
974
999
  if logger && @config_opts[:log_level] != false
975
- cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
1000
+ cmd_str = @config_opts[:log_cmd] || default_log_str
976
1001
  logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
977
1002
  end
978
1003
  end
979
1004
 
980
- def default_log_str(spawn_cmd)
981
- return nil unless spawn_cmd
982
- return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
983
- cmd_binary = spawn_cmd.first
984
- cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
985
- ([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
986
1017
  end
987
1018
 
988
1019
  def start_with_controller
@@ -1093,9 +1124,10 @@ module Toys
1093
1124
 
1094
1125
  def interpret_out_array_within_fork(stream)
1095
1126
  if stream.first == :child
1096
- if stream[1] == :err
1127
+ case stream[1]
1128
+ when :err
1097
1129
  $stderr
1098
- elsif stream[1] == :out
1130
+ when :out
1099
1131
  $stdout
1100
1132
  end
1101
1133
  else
@@ -1312,8 +1344,6 @@ module Toys
1312
1344
  end
1313
1345
  end
1314
1346
 
1315
- private
1316
-
1317
1347
  def canonical_binary_spec(cmd, exec_opts)
1318
1348
  config_argv0 = exec_opts.config_opts[:argv0]
1319
1349
  return cmd.to_s if !config_argv0 && !cmd.is_a?(::Array)
@@ -132,8 +132,8 @@ module Toys
132
132
  end
133
133
  @on_conflict = on_conflict || :error
134
134
  @terminal = terminal
135
- @input = input || ::STDIN
136
- @output = output || ::STDOUT
135
+ @input = input || $stdin
136
+ @output = output || $stdout
137
137
  end
138
138
 
139
139
  ##
@@ -185,17 +185,19 @@ module Toys
185
185
  gemfile_path = Gems.find_gemfile(dir, gemfile_names: gemfile_names)
186
186
  end
187
187
  raise GemfileNotFoundError, "Gemfile not found" unless gemfile_path
188
+ gemfile_path = ::File.absolute_path(gemfile_path)
188
189
  Gems.synchronize do
189
190
  if configure_gemfile(gemfile_path)
190
- activate("bundler", "~> 2.1")
191
+ activate("bundler", "~> 2.2")
191
192
  require "bundler"
192
- lockfile_path = find_lockfile_path(gemfile_path)
193
- setup_bundle(gemfile_path, lockfile_path, groups: groups, retries: retries)
193
+ setup_bundle(gemfile_path, groups: groups, retries: retries)
194
194
  end
195
195
  end
196
196
  end
197
197
 
198
+ ##
198
199
  # @private
200
+ #
199
201
  def self.find_gemfile(search_dir, gemfile_names: nil)
200
202
  gemfile_names ||= DEFAULT_GEMFILE_NAMES
201
203
  Array(gemfile_names).each do |file|
@@ -207,7 +209,9 @@ module Toys
207
209
 
208
210
  @global_mutex = ::Monitor.new
209
211
 
212
+ ##
210
213
  # @private
214
+ #
211
215
  def self.synchronize(&block)
212
216
  @global_mutex.synchronize(&block)
213
217
  end
@@ -254,8 +258,7 @@ module Toys
254
258
  def confirm_and_install_gem(name, requirements)
255
259
  if @on_missing == :confirm
256
260
  requirements_text = gem_requirements_text(name, requirements)
257
- response = terminal.confirm("Gem needed: #{requirements_text}. Install? ",
258
- default: @default_confirm)
261
+ response = terminal.confirm("Gem needed: #{requirements_text}. Install? ", default: @default_confirm)
259
262
  unless response
260
263
  raise InstallFailedError, "Canceled installation of needed gem: #{requirements_text}"
261
264
  end
@@ -296,75 +299,96 @@ module Toys
296
299
  if ::File.basename(gemfile_path) == "gems.rb"
297
300
  ::File.join(::File.dirname(gemfile_path), "gems.locked")
298
301
  else
299
- gemfile_path + ".lock"
302
+ "#{gemfile_path}.lock"
300
303
  end
301
304
  end
302
305
 
303
- 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)
304
308
  groups = Array(groups)
305
- old_lockfile_contents = save_old_lockfile(lockfile_path)
309
+ modified_gemfile_path = create_modified_gemfile(gemfile_path)
306
310
  begin
307
- modify_bundle_definition(gemfile_path, lockfile_path)
308
- ::Bundler.ui.silence { ::Bundler.setup(*groups) }
311
+ attempt_setup_bundle(modified_gemfile_path, groups)
309
312
  rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
310
- restore_toys_libs
311
- install_bundle(gemfile_path, retries: retries)
312
- old_lockfile_contents = save_old_lockfile(lockfile_path)
313
313
  ::Bundler.reset!
314
- modify_bundle_definition(gemfile_path, lockfile_path)
315
- ::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
316
320
  end
317
321
  restore_toys_libs
318
- ensure
319
- restore_old_lockfile(lockfile_path, old_lockfile_contents)
320
- end
321
-
322
- def save_old_lockfile(lockfile_path)
323
- return nil unless ::File.readable?(lockfile_path) && ::File.writable?(lockfile_path)
324
- ::File.read(lockfile_path)
325
322
  end
326
323
 
327
- def restore_old_lockfile(lockfile_path, contents)
328
- if contents
329
- ::File.open(lockfile_path, "w") do |file|
330
- 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)
331
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)
335
+ ::Bundler.configure
336
336
  builder = ::Bundler::Dsl.new
337
337
  builder.eval_gemfile(gemfile_path)
338
- toys_gems = ["toys-core"]
339
- remove_gem_from_definition(builder, "toys-core")
340
- removed_toys = remove_gem_from_definition(builder, "toys")
341
- add_gem_to_definition(builder, "toys-core")
342
- if removed_toys || ::Toys.const_defined?(:VERSION)
343
- add_gem_to_definition(builder, "toys")
344
- toys_gems << "toys"
345
- end
346
- definition = builder.to_definition(lockfile_path, { gems: toys_gems })
347
- ::Bundler.instance_variable_set(:@definition, definition)
338
+ check_gemfile_gem_compatibility(builder, "toys-core")
339
+ check_gemfile_gem_compatibility(builder, "toys")
340
+ ::Bundler.reset!
348
341
  end
349
342
 
350
- def remove_gem_from_definition(builder, name)
343
+ def check_gemfile_gem_compatibility(builder, name)
351
344
  existing_dep = builder.dependencies.find { |dep| dep.name == name }
352
- return false unless existing_dep
353
- 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))
354
346
  raise IncompatibleToysError,
355
347
  "The bundle lists #{name} #{existing_dep.requirement} as a dependency, which is" \
356
- " incompatible with the current version #{::Toys::Core::VERSION}."
348
+ " incompatible with the current toys version #{::Toys::Core::VERSION}."
357
349
  end
358
- builder.dependencies.delete(existing_dep)
359
- true
360
350
  end
361
351
 
362
- def add_gem_to_definition(builder, name)
363
- if ::ENV["TOYS_DEV"] == "true"
364
- 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)
365
359
  end
366
- command = "gem #{name.inspect}, #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}\n"
367
- builder.eval_gemfile("current #{name}", command)
360
+ ::File.open(modified_gemfile_path, "w") do |file|
361
+ modified_gemfile_content(gemfile_path).each do |line|
362
+ file.puts(line)
363
+ end
364
+ end
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)
368
392
  end
369
393
 
370
394
  def restore_toys_libs
@@ -383,8 +407,7 @@ module Toys
383
407
  when :error
384
408
  false
385
409
  else
386
- terminal.confirm("Your bundle requires additional gems. Install? ",
387
- default: @default_confirm)
410
+ terminal.confirm("Your bundle requires additional gems. Install? ", default: @default_confirm)
388
411
  end
389
412
  end
390
413
 
@@ -392,17 +415,19 @@ module Toys
392
415
  gemfile_dir = ::File.dirname(gemfile_path)
393
416
  unless permission_to_bundle?
394
417
  raise BundleNotInstalledError,
395
- "Your bundle is not installed. Consider running" \
396
- " `cd #{gemfile_dir} && bundle install`"
418
+ "Your bundle is not installed. Consider running `cd #{gemfile_dir} && bundle install`"
397
419
  end
398
420
  retries = retries.to_i
399
- args = retries.positive? ? ["--retry=#{retries}"] : []
400
- require "bundler/cli"
401
- begin
402
- ::Bundler::CLI.start(["install"] + args)
403
- 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?
404
426
  terminal.puts("Failed to install. Trying update...")
405
- ::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
406
431
  end
407
432
  end
408
433
  end