toys-core 0.10.4 → 0.11.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -34
- data/README.md +5 -5
- data/docs/guide.md +2 -0
- data/lib/toys/acceptor.rb +1 -1
- data/lib/toys/compat.rb +5 -1
- data/lib/toys/core.rb +1 -1
- data/lib/toys/dsl/flag.rb +2 -2
- data/lib/toys/dsl/flag_group.rb +2 -2
- data/lib/toys/dsl/tool.rb +27 -6
- data/lib/toys/loader.rb +71 -31
- data/lib/toys/source_info.rb +22 -23
- data/lib/toys/standard_mixins/bundler.rb +113 -56
- data/lib/toys/utils/exec.rb +126 -34
- data/lib/toys/utils/gems.rb +82 -31
- data/lib/toys/utils/help_text.rb +54 -59
- metadata +4 -5
- data/lib/toys/utils/gems/gemfile.rb +0 -6
data/lib/toys/utils/exec.rb
CHANGED
@@ -250,13 +250,14 @@ module Toys
|
|
250
250
|
exec_opts = Opts.new(@default_opts).add(opts)
|
251
251
|
spawn_cmd =
|
252
252
|
if cmd.is_a?(::Array)
|
253
|
-
if cmd.size
|
254
|
-
|
253
|
+
if cmd.size > 1
|
254
|
+
binary = canonical_binary_spec(cmd.first, exec_opts)
|
255
|
+
[binary] + cmd[1..-1].map(&:to_s)
|
255
256
|
else
|
256
|
-
cmd
|
257
|
+
[canonical_binary_spec(Array(cmd.first), exec_opts)]
|
257
258
|
end
|
258
259
|
else
|
259
|
-
[cmd]
|
260
|
+
[cmd.to_s]
|
260
261
|
end
|
261
262
|
executor = Executor.new(exec_opts, spawn_cmd, block)
|
262
263
|
executor.execute
|
@@ -492,7 +493,8 @@ module Toys
|
|
492
493
|
#
|
493
494
|
class Controller
|
494
495
|
## @private
|
495
|
-
def initialize(name, controller_streams, captures, pid, join_threads,
|
496
|
+
def initialize(name, controller_streams, captures, pid, join_threads,
|
497
|
+
result_callback, mutex)
|
496
498
|
@name = name
|
497
499
|
@in = controller_streams[:in]
|
498
500
|
@out = controller_streams[:out]
|
@@ -508,6 +510,7 @@ module Toys
|
|
508
510
|
end
|
509
511
|
@join_threads = join_threads
|
510
512
|
@result_callback = result_callback
|
513
|
+
@mutex = mutex
|
511
514
|
@result = nil
|
512
515
|
end
|
513
516
|
|
@@ -547,7 +550,7 @@ module Toys
|
|
547
550
|
##
|
548
551
|
# The process ID.
|
549
552
|
#
|
550
|
-
# Exactly one of
|
553
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
551
554
|
#
|
552
555
|
# @return [Integer] if the process start was successful
|
553
556
|
# @return [nil] if the process could not be started.
|
@@ -557,7 +560,7 @@ module Toys
|
|
557
560
|
##
|
558
561
|
# The exception raised when the process failed to start.
|
559
562
|
#
|
560
|
-
# Exactly one of
|
563
|
+
# Exactly one of {#exception} and {#pid} will be non-nil.
|
561
564
|
#
|
562
565
|
# @return [Exception] if the process failed to start.
|
563
566
|
# @return [nil] if the process start was successful.
|
@@ -575,7 +578,10 @@ module Toys
|
|
575
578
|
stream = stream_for(which)
|
576
579
|
@join_threads << ::Thread.new do
|
577
580
|
begin
|
578
|
-
|
581
|
+
data = stream.read
|
582
|
+
@mutex.synchronize do
|
583
|
+
@captures[which] = data
|
584
|
+
end
|
579
585
|
ensure
|
580
586
|
stream.close
|
581
587
|
end
|
@@ -722,15 +728,20 @@ module Toys
|
|
722
728
|
##
|
723
729
|
# Wait for the subcommand to complete, and return a result object.
|
724
730
|
#
|
731
|
+
# Closes the control streams if present. The stdin stream is always
|
732
|
+
# closed, even if the call times out. The stdout and stderr streams are
|
733
|
+
# closed only after the command terminates.
|
734
|
+
#
|
725
735
|
# @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
|
726
736
|
# wait indefinitely.
|
727
737
|
# @return [Toys::Utils::Exec::Result] The result object
|
728
738
|
# @return [nil] if a timeout occurred.
|
729
739
|
#
|
730
740
|
def result(timeout: nil)
|
741
|
+
close_streams(:in)
|
731
742
|
return nil if @wait_thread && !@wait_thread.join(timeout)
|
732
743
|
@result ||= begin
|
733
|
-
close_streams
|
744
|
+
close_streams(:out)
|
734
745
|
@join_threads.each(&:join)
|
735
746
|
Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
|
736
747
|
.tap { |result| @result_callback&.call(result) }
|
@@ -738,13 +749,13 @@ module Toys
|
|
738
749
|
end
|
739
750
|
|
740
751
|
##
|
741
|
-
# Close
|
752
|
+
# Close the controller's streams.
|
742
753
|
# @private
|
743
754
|
#
|
744
|
-
def close_streams
|
745
|
-
@in.close if @in && !@in.closed?
|
746
|
-
@out.close if @out && !@out.closed?
|
747
|
-
@err.close if @err && !@err.closed?
|
755
|
+
def close_streams(which)
|
756
|
+
@in.close if which != :out && @in && !@in.closed?
|
757
|
+
@out.close if which != :in && @out && !@out.closed?
|
758
|
+
@err.close if which != :in && @err && !@err.closed?
|
748
759
|
self
|
749
760
|
end
|
750
761
|
|
@@ -773,7 +784,21 @@ module Toys
|
|
773
784
|
end
|
774
785
|
|
775
786
|
##
|
776
|
-
# The result returned from a subcommand execution.
|
787
|
+
# The result returned from a subcommand execution. This includes the
|
788
|
+
# identifying name of the execution (if any), the result status of the
|
789
|
+
# execution, and any captured stream output.
|
790
|
+
#
|
791
|
+
# Possible result statuses are:
|
792
|
+
#
|
793
|
+
# * The process failed to start. {Result#failed?} will return true, and
|
794
|
+
# {Result#exception} will return an exception describing the failure
|
795
|
+
# (often an errno).
|
796
|
+
# * The process executed and exited with a normal exit code. Either
|
797
|
+
# {Result#success?} or {Result#error?} will return true, and
|
798
|
+
# {Result.exit_code} will return the numeric exit code.
|
799
|
+
# * The process executed but was terminated by an uncaught signal.
|
800
|
+
# {Result#signaled?} will return true, and {Result#term_signal} will
|
801
|
+
# return the numeric signal code.
|
777
802
|
#
|
778
803
|
class Result
|
779
804
|
## @private
|
@@ -809,11 +834,13 @@ module Toys
|
|
809
834
|
attr_reader :captured_err
|
810
835
|
|
811
836
|
##
|
812
|
-
# The status
|
837
|
+
# The Ruby process status object, providing various information about
|
838
|
+
# the ending state of the process.
|
813
839
|
#
|
814
|
-
# Exactly one of
|
840
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
815
841
|
#
|
816
|
-
# @return [Process::Status] The status
|
842
|
+
# @return [Process::Status] The status, if the process was successfully
|
843
|
+
# spanwed and terminated.
|
817
844
|
# @return [nil] if the process could not be started.
|
818
845
|
#
|
819
846
|
attr_reader :status
|
@@ -821,7 +848,7 @@ module Toys
|
|
821
848
|
##
|
822
849
|
# The exception raised if a process couldn't be started.
|
823
850
|
#
|
824
|
-
# Exactly one of
|
851
|
+
# Exactly one of {#exception} and {#status} will be non-nil.
|
825
852
|
#
|
826
853
|
# @return [Exception] The exception raised from process start.
|
827
854
|
# @return [nil] if the process started successfully.
|
@@ -829,33 +856,76 @@ module Toys
|
|
829
856
|
attr_reader :exception
|
830
857
|
|
831
858
|
##
|
832
|
-
# The numeric status code
|
859
|
+
# The numeric status code for a process that exited normally,
|
833
860
|
#
|
834
|
-
#
|
835
|
-
#
|
861
|
+
# Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
|
862
|
+
# non-nil.
|
836
863
|
#
|
837
|
-
# @return [Integer]
|
864
|
+
# @return [Integer] the numeric status code, if the process started
|
865
|
+
# successfully and exited normally.
|
866
|
+
# @return [nil] if the process did not start successfully, or was
|
867
|
+
# terminated by an uncaught signal.
|
838
868
|
#
|
839
869
|
def exit_code
|
840
|
-
status
|
870
|
+
status&.exitstatus
|
871
|
+
end
|
872
|
+
|
873
|
+
##
|
874
|
+
# The numeric signal code that caused process termination.
|
875
|
+
#
|
876
|
+
# Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
|
877
|
+
# non-nil.
|
878
|
+
#
|
879
|
+
# @return [Integer] The signal that caused the process to terminate.
|
880
|
+
# @return [nil] if the process did not start successfully, or executed
|
881
|
+
# and exited with a normal exit code.
|
882
|
+
#
|
883
|
+
def term_signal
|
884
|
+
status&.termsig
|
885
|
+
end
|
886
|
+
|
887
|
+
##
|
888
|
+
# Returns true if the subprocess failed to start, or false if the
|
889
|
+
# process was able to execute.
|
890
|
+
#
|
891
|
+
# @return [Boolean]
|
892
|
+
#
|
893
|
+
def failed?
|
894
|
+
status.nil?
|
895
|
+
end
|
896
|
+
|
897
|
+
##
|
898
|
+
# Returns true if the subprocess terminated due to an unhandled signal,
|
899
|
+
# or false if the process failed to start or exited normally.
|
900
|
+
#
|
901
|
+
# @return [Boolean]
|
902
|
+
#
|
903
|
+
def signaled?
|
904
|
+
!term_signal.nil?
|
841
905
|
end
|
842
906
|
|
843
907
|
##
|
844
|
-
# Returns true if the subprocess terminated with a zero status
|
908
|
+
# Returns true if the subprocess terminated with a zero status, or
|
909
|
+
# false if the process failed to start, terminated due to a signal, or
|
910
|
+
# returned a nonzero status.
|
845
911
|
#
|
846
912
|
# @return [Boolean]
|
847
913
|
#
|
848
914
|
def success?
|
849
|
-
exit_code
|
915
|
+
code = exit_code
|
916
|
+
!code.nil? && code.zero?
|
850
917
|
end
|
851
918
|
|
852
919
|
##
|
853
|
-
# Returns true if the subprocess terminated with a nonzero status
|
920
|
+
# Returns true if the subprocess terminated with a nonzero status, or
|
921
|
+
# false if the process failed to start, terminated due to a signal, or
|
922
|
+
# returned a zero status.
|
854
923
|
#
|
855
924
|
# @return [Boolean]
|
856
925
|
#
|
857
926
|
def error?
|
858
|
-
|
927
|
+
code = exit_code
|
928
|
+
!code.nil? && !code.zero?
|
859
929
|
end
|
860
930
|
end
|
861
931
|
|
@@ -876,6 +946,7 @@ module Toys
|
|
876
946
|
@parent_streams = []
|
877
947
|
@block = block
|
878
948
|
@default_stream = @config_opts[:background] ? :null : :inherit
|
949
|
+
@mutex = ::Mutex.new
|
879
950
|
end
|
880
951
|
|
881
952
|
def execute
|
@@ -887,10 +958,10 @@ module Toys
|
|
887
958
|
return controller if @config_opts[:background]
|
888
959
|
begin
|
889
960
|
@block&.call(controller)
|
961
|
+
controller.result
|
890
962
|
ensure
|
891
|
-
controller.close_streams
|
963
|
+
controller.close_streams(:both)
|
892
964
|
end
|
893
|
-
controller.result
|
894
965
|
end
|
895
966
|
|
896
967
|
private
|
@@ -898,12 +969,19 @@ module Toys
|
|
898
969
|
def log_command
|
899
970
|
logger = @config_opts[:logger]
|
900
971
|
if logger && @config_opts[:log_level] != false
|
901
|
-
cmd_str = @config_opts[:log_cmd]
|
902
|
-
cmd_str ||= @spawn_cmd.size == 1 ? @spawn_cmd.first : @spawn_cmd.inspect if @spawn_cmd
|
972
|
+
cmd_str = @config_opts[:log_cmd] || default_log_str(@spawn_cmd)
|
903
973
|
logger.add(@config_opts[:log_level] || ::Logger::INFO, cmd_str) if cmd_str
|
904
974
|
end
|
905
975
|
end
|
906
976
|
|
977
|
+
def default_log_str(spawn_cmd)
|
978
|
+
return nil unless spawn_cmd
|
979
|
+
return spawn_cmd.first if spawn_cmd.size == 1 && spawn_cmd.first.is_a?(::String)
|
980
|
+
cmd_binary = spawn_cmd.first
|
981
|
+
cmd_binary = cmd_binary.first if cmd_binary.is_a?(::Array)
|
982
|
+
([cmd_binary] + spawn_cmd[1..-1]).inspect
|
983
|
+
end
|
984
|
+
|
907
985
|
def start_with_controller
|
908
986
|
pid =
|
909
987
|
begin
|
@@ -913,7 +991,7 @@ module Toys
|
|
913
991
|
end
|
914
992
|
@child_streams.each(&:close)
|
915
993
|
Controller.new(@config_opts[:name], @controller_streams, @captures, pid,
|
916
|
-
@join_threads, @config_opts[:result_callback])
|
994
|
+
@join_threads, @config_opts[:result_callback], @mutex)
|
917
995
|
end
|
918
996
|
|
919
997
|
def start_process
|
@@ -1220,13 +1298,27 @@ module Toys
|
|
1220
1298
|
stream = make_out_pipe(key)
|
1221
1299
|
@join_threads << ::Thread.new do
|
1222
1300
|
begin
|
1223
|
-
|
1301
|
+
data = stream.read
|
1302
|
+
@mutex.synchronize do
|
1303
|
+
@captures[key] = data
|
1304
|
+
end
|
1224
1305
|
ensure
|
1225
1306
|
stream.close
|
1226
1307
|
end
|
1227
1308
|
end
|
1228
1309
|
end
|
1229
1310
|
end
|
1311
|
+
|
1312
|
+
private
|
1313
|
+
|
1314
|
+
def canonical_binary_spec(cmd, exec_opts)
|
1315
|
+
config_argv0 = exec_opts.config_opts[:argv0]
|
1316
|
+
return cmd.to_s if !config_argv0 && !cmd.is_a?(::Array)
|
1317
|
+
cmd = Array(cmd)
|
1318
|
+
actual_cmd = cmd.first
|
1319
|
+
argv0 = cmd[1] || config_argv0 || actual_cmd
|
1320
|
+
[actual_cmd.to_s, argv0.to_s]
|
1321
|
+
end
|
1230
1322
|
end
|
1231
1323
|
end
|
1232
1324
|
end
|
data/lib/toys/utils/gems.rb
CHANGED
@@ -64,6 +64,19 @@ module Toys
|
|
64
64
|
class AlreadyBundledError < BundlerFailedError
|
65
65
|
end
|
66
66
|
|
67
|
+
##
|
68
|
+
# The bundle contained a toys or toys-core dependency that is
|
69
|
+
# incompatible with the currently running version.
|
70
|
+
#
|
71
|
+
class IncompatibleToysError < BundlerFailedError
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# The gemfile names that are searched by default.
|
76
|
+
# @return [Array<String>]
|
77
|
+
#
|
78
|
+
DEFAULT_GEMFILE_NAMES = [".gems.rb", "gems.rb", "Gemfile"].freeze
|
79
|
+
|
67
80
|
##
|
68
81
|
# Activate the given gem. If it is not present, attempt to install it (or
|
69
82
|
# inform the user to update the bundle).
|
@@ -142,16 +155,33 @@ module Toys
|
|
142
155
|
end
|
143
156
|
|
144
157
|
##
|
145
|
-
#
|
158
|
+
# Search for an appropriate Gemfile, and set up the bundle.
|
159
|
+
#
|
160
|
+
# @param groups [Array<String>] The groups to include in setup.
|
161
|
+
#
|
162
|
+
# @param gemfile_path [String] The path to the Gemfile to use. If `nil`
|
163
|
+
# or not given, the `:search_dirs` will be searched for a Gemfile.
|
164
|
+
#
|
165
|
+
# @param search_dirs [String,Array<String>] Directories in which to
|
166
|
+
# search for a Gemfile, if gemfile_path is not given. You can provide
|
167
|
+
# a single directory or an array of directories.
|
168
|
+
#
|
169
|
+
# @param gemfile_names [String,Array<String>] File names that are
|
170
|
+
# recognized as Gemfiles, when searching because gemfile_path is not
|
171
|
+
# given. Defaults to {DEFAULT_GEMFILE_NAMES}.
|
146
172
|
#
|
147
|
-
# @param groups [Array<String>] The groups to include in setup
|
148
|
-
# @param search_dirs [Array<String>] Directories to search for a Gemfile
|
149
173
|
# @return [void]
|
150
174
|
#
|
151
175
|
def bundle(groups: nil,
|
152
|
-
|
176
|
+
gemfile_path: nil,
|
177
|
+
search_dirs: nil,
|
178
|
+
gemfile_names: nil)
|
179
|
+
Array(search_dirs).each do |dir|
|
180
|
+
break if gemfile_path
|
181
|
+
gemfile_path = Gems.find_gemfile(dir, gemfile_names: gemfile_names)
|
182
|
+
end
|
183
|
+
raise GemfileNotFoundError, "Gemfile not found" unless gemfile_path
|
153
184
|
Gems.synchronize do
|
154
|
-
gemfile_path = find_gemfile(Array(search_dirs))
|
155
185
|
if configure_gemfile(gemfile_path)
|
156
186
|
activate("bundler", "~> 2.1")
|
157
187
|
require "bundler"
|
@@ -160,9 +190,19 @@ module Toys
|
|
160
190
|
end
|
161
191
|
end
|
162
192
|
|
193
|
+
# @private
|
194
|
+
def self.find_gemfile(search_dir, gemfile_names: nil)
|
195
|
+
gemfile_names ||= DEFAULT_GEMFILE_NAMES
|
196
|
+
Array(gemfile_names).each do |file|
|
197
|
+
gemfile_path = ::File.join(search_dir, file)
|
198
|
+
return gemfile_path if ::File.readable?(gemfile_path)
|
199
|
+
end
|
200
|
+
nil
|
201
|
+
end
|
202
|
+
|
163
203
|
@global_mutex = ::Monitor.new
|
164
204
|
|
165
|
-
|
205
|
+
# @private
|
166
206
|
def self.synchronize(&block)
|
167
207
|
@global_mutex.synchronize(&block)
|
168
208
|
end
|
@@ -230,14 +270,6 @@ module Toys
|
|
230
270
|
raise ActivationFailedError, err.message
|
231
271
|
end
|
232
272
|
|
233
|
-
def find_gemfile(search_dirs)
|
234
|
-
search_dirs.each do |dir|
|
235
|
-
gemfile_path = ::File.join(dir, "Gemfile")
|
236
|
-
return gemfile_path if ::File.readable?(gemfile_path)
|
237
|
-
end
|
238
|
-
raise GemfileNotFoundError, "Gemfile not found"
|
239
|
-
end
|
240
|
-
|
241
273
|
def configure_gemfile(gemfile_path)
|
242
274
|
old_path = ::ENV["BUNDLE_GEMFILE"]
|
243
275
|
if old_path
|
@@ -258,13 +290,13 @@ module Toys
|
|
258
290
|
def setup_bundle(gemfile_path, groups)
|
259
291
|
begin
|
260
292
|
modify_bundle_definition(gemfile_path)
|
261
|
-
::Bundler.setup(*groups)
|
262
|
-
rescue ::Bundler::GemNotFound
|
293
|
+
::Bundler.ui.silence { ::Bundler.setup(*groups) }
|
294
|
+
rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
|
263
295
|
restore_toys_libs
|
264
296
|
install_bundle(gemfile_path)
|
265
297
|
::Bundler.reset!
|
266
298
|
modify_bundle_definition(gemfile_path)
|
267
|
-
::Bundler.setup(*groups)
|
299
|
+
::Bundler.ui.silence { ::Bundler.setup(*groups) }
|
268
300
|
end
|
269
301
|
restore_toys_libs
|
270
302
|
end
|
@@ -272,24 +304,38 @@ module Toys
|
|
272
304
|
def modify_bundle_definition(gemfile_path)
|
273
305
|
builder = ::Bundler::Dsl.new
|
274
306
|
builder.eval_gemfile(gemfile_path)
|
275
|
-
begin
|
276
|
-
builder.eval_gemfile(::File.join(__dir__, "gems", "gemfile.rb"))
|
277
|
-
rescue ::Bundler::Dsl::DSLError
|
278
|
-
terminal.puts(
|
279
|
-
"WARNING: Unable to integrate your Gemfile into the Toys runtime.\n" \
|
280
|
-
"When using the Toys Bundler integration features, do NOT list\n" \
|
281
|
-
"the toys or toys-core gems directly in your Gemfile. They can be\n" \
|
282
|
-
"dependencies of another gem, but cannot be listed directly.",
|
283
|
-
:red
|
284
|
-
)
|
285
|
-
return
|
286
|
-
end
|
287
307
|
toys_gems = ["toys-core"]
|
288
|
-
|
308
|
+
remove_gem_from_definition(builder, "toys-core")
|
309
|
+
removed_toys = remove_gem_from_definition(builder, "toys")
|
310
|
+
add_gem_to_definition(builder, "toys-core")
|
311
|
+
if removed_toys || ::Toys.const_defined?(:VERSION)
|
312
|
+
add_gem_to_definition(builder, "toys")
|
313
|
+
toys_gems << "toys"
|
314
|
+
end
|
289
315
|
definition = builder.to_definition(gemfile_path + ".lock", { gems: toys_gems })
|
290
316
|
::Bundler.instance_variable_set(:@definition, definition)
|
291
317
|
end
|
292
318
|
|
319
|
+
def remove_gem_from_definition(builder, name)
|
320
|
+
existing_dep = builder.dependencies.find { |dep| dep.name == name }
|
321
|
+
return false unless existing_dep
|
322
|
+
unless existing_dep.requirement.satisfied_by?(::Gem::Version.new(::Toys::Core::VERSION))
|
323
|
+
raise IncompatibleToysError,
|
324
|
+
"The bundle lists #{name} #{existing_dep.requirement} as a dependency, which is" \
|
325
|
+
" incompatible with the current version #{::Toys::Core::VERSION}."
|
326
|
+
end
|
327
|
+
builder.dependencies.delete(existing_dep)
|
328
|
+
true
|
329
|
+
end
|
330
|
+
|
331
|
+
def add_gem_to_definition(builder, name)
|
332
|
+
if ::ENV["TOYS_DEV"] == "true"
|
333
|
+
path = ::File.join(::File.dirname(::File.dirname(::Toys::CORE_LIB_PATH)), name)
|
334
|
+
end
|
335
|
+
command = "gem #{name.inspect}, #{::Toys::Core::VERSION.inspect}, path: #{path.inspect}\n"
|
336
|
+
builder.eval_gemfile("current #{name}", command)
|
337
|
+
end
|
338
|
+
|
293
339
|
def restore_toys_libs
|
294
340
|
$LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
|
295
341
|
$LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
|
@@ -319,7 +365,12 @@ module Toys
|
|
319
365
|
" `cd #{gemfile_dir} && bundle install`"
|
320
366
|
end
|
321
367
|
require "bundler/cli"
|
322
|
-
|
368
|
+
begin
|
369
|
+
::Bundler::CLI.start(["install"])
|
370
|
+
rescue ::Bundler::GemNotFound, ::Bundler::InstallError
|
371
|
+
terminal.puts("Failed to install. Trying update...")
|
372
|
+
::Bundler::CLI.start(["update"])
|
373
|
+
end
|
323
374
|
end
|
324
375
|
end
|
325
376
|
end
|