toys-core 0.11.5 → 0.13.0

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