toys-core 0.10.1 → 0.11.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.
@@ -7,32 +7,55 @@ module Toys
7
7
  #
8
8
  # The following parameters can be passed when including this mixin:
9
9
  #
10
- # * `:groups` (Array<String>) The groups to include in setup
10
+ # * `:static` (Boolean) If `true`, installs the bundle immediately, when
11
+ # defining the tool. If `false` (the default), installs the bundle just
12
+ # before the tool runs.
11
13
  #
12
- # * `:search_dirs` (Array<String,Symbol>) Directories to search for a
13
- # Gemfile.
14
+ # * `:groups` (Array\<String\>) The groups to include in setup.
15
+ #
16
+ # * `:gemfile_path` (String) The path to the Gemfile to use. If `nil` or
17
+ # not given, the `:search_dirs` will be searched for a Gemfile.
18
+ #
19
+ # * `:search_dirs` (String,Symbol,Array\<String,Symbol\>) Directories to
20
+ # search for a Gemfile.
14
21
  #
15
22
  # You can pass full directory paths, and/or any of the following:
16
- # * `:context` - the current context directory
17
- # * `:current` - the current working directory
18
- # * `:toys` - the Toys directory containing the tool definition
23
+ # * `:context` - the current context directory.
24
+ # * `:current` - the current working directory.
25
+ # * `:toys` - the Toys directory containing the tool definition, and
26
+ # any of its parents within the Toys directory hierarchy.
19
27
  #
20
28
  # The default is to search `[:toys, :context, :current]` in that order.
29
+ # See {DEFAULT_SEARCH_DIRS}.
30
+ #
31
+ # For most directories, the bundler mixin will look for the files
32
+ # ".gems.rb", "gems.rb", and "Gemfile", in that order. In `:toys`
33
+ # directories, it will look only for ".gems.rb" and "Gemfile", in that
34
+ # order. These can be overridden by setting the `:gemfile_names` and/or
35
+ # `:toys_gemfile_names` arguments.
36
+ #
37
+ # * `:gemfile_names` (Array\<String\>) File names that are recognized as
38
+ # Gemfiles when searching in directories other than Toys directories.
39
+ # Defaults to {Toys::Utils::Gems::DEFAULT_GEMFILE_NAMES}.
40
+ #
41
+ # * `:toys_gemfile_names` (Array\<String\>) File names that are
42
+ # recognized as Gemfiles when wearching in Toys directories.
43
+ # Defaults to {DEFAULT_TOYS_GEMFILE_NAMES}.
21
44
  #
22
45
  # * `:on_missing` (Symbol) What to do if a needed gem is not installed.
23
46
  #
24
47
  # Supported values:
25
- # * `:confirm` - prompt the user on whether to install (default)
26
- # * `:error` - raise an exception
27
- # * `:install` - just install the gem
48
+ # * `:confirm` - prompt the user on whether to install (default).
49
+ # * `:error` - raise an exception.
50
+ # * `:install` - just install the gem.
28
51
  #
29
52
  # * `:on_conflict` (Symbol) What to do if bundler has already been run
30
53
  # with a different Gemfile.
31
54
  #
32
55
  # Supported values:
33
- # * `:error` - raise an exception (default)
34
- # * `:ignore` - just silently proceed without bundling again
35
- # * `:warn` - print a warning and proceed without bundling again
56
+ # * `:error` - raise an exception (default).
57
+ # * `:ignore` - just silently proceed without bundling again.
58
+ # * `:warn` - print a warning and proceed without bundling again.
36
59
  #
37
60
  # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional)
38
61
  # * `:input` (IO) Input IO (optional, defaults to STDIN)
@@ -41,48 +64,106 @@ module Toys
41
64
  module Bundler
42
65
  include Mixin
43
66
 
44
- on_initialize do
45
- |groups: nil,
46
- search_dirs: nil,
47
- on_missing: nil,
48
- on_conflict: nil,
49
- terminal: nil,
50
- input: nil,
51
- output: nil|
67
+ on_initialize do |static: false, **kwargs|
68
+ unless static
69
+ context_directory = self[::Toys::Context::Key::CONTEXT_DIRECTORY]
70
+ source_info = self[::Toys::Context::Key::TOOL_SOURCE]
71
+ ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
72
+ end
73
+ end
74
+
75
+ on_include do |static: false, **kwargs|
76
+ if static
77
+ ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
78
+ end
79
+ end
80
+
81
+ ##
82
+ # Default search directories for Gemfiles.
83
+ # @return [Array<String,Symbol>]
84
+ #
85
+ DEFAULT_SEARCH_DIRS = [:toys, :context, :current].freeze
86
+
87
+ ##
88
+ # The gemfile names that are searched by default in Toys directories.
89
+ # @return [Array<String>]
90
+ #
91
+ DEFAULT_TOYS_GEMFILE_NAMES = [".gems.rb", "Gemfile"].freeze
92
+
93
+ # @private
94
+ def self.setup_bundle(context_directory,
95
+ source_info,
96
+ gemfile_path: nil,
97
+ search_dirs: nil,
98
+ gemfile_names: nil,
99
+ toys_gemfile_names: nil,
100
+ groups: nil,
101
+ on_missing: nil,
102
+ on_conflict: nil,
103
+ terminal: nil,
104
+ input: nil,
105
+ output: nil)
52
106
  require "toys/utils/gems"
53
- search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(search_dirs, self)
107
+ gemfile_path ||= begin
108
+ gemfile_finder = GemfileFinder.new(context_directory, source_info,
109
+ gemfile_names, toys_gemfile_names)
110
+ gemfile_finder.search(search_dirs || DEFAULT_SEARCH_DIRS)
111
+ end
54
112
  gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict,
55
113
  terminal: terminal, input: input, output: output)
56
- gems.bundle(groups: groups, search_dirs: search_dirs)
114
+ gems.bundle(groups: groups, gemfile_path: gemfile_path)
57
115
  end
58
116
 
59
- ## @private
60
- def self.resolve_search_dirs(search_dirs, context)
61
- search_dirs ||= [:toys, :context, :current]
62
- Array(search_dirs).flat_map do |dir|
63
- case dir
117
+ # @private
118
+ class GemfileFinder
119
+ # @private
120
+ def initialize(context_directory, source_info, gemfile_names, toys_gemfile_names)
121
+ @context_directory = context_directory
122
+ @source_info = source_info
123
+ @gemfile_names = gemfile_names
124
+ @toys_gemfile_names = toys_gemfile_names || DEFAULT_TOYS_GEMFILE_NAMES
125
+ end
126
+
127
+ # @private
128
+ def search(search_dir)
129
+ case search_dir
130
+ when ::Array
131
+ search_array(search_dir)
132
+ when ::String
133
+ ::Toys::Utils::Gems.find_gemfile(search_dir, gemfile_names: @gemfile_names)
64
134
  when :context
65
- context[::Toys::Context::Key::CONTEXT_DIRECTORY]
135
+ search(@context_directory)
66
136
  when :current
67
- ::Dir.getwd
137
+ search(::Dir.getwd)
68
138
  when :toys
69
- toys_dir_stack(context[::Toys::Context::Key::TOOL_SOURCE])
70
- when ::String
71
- dir
139
+ search_toys
72
140
  else
73
141
  raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
74
142
  end
75
143
  end
76
- end
77
144
 
78
- ## @private
79
- def self.toys_dir_stack(source_info)
80
- dirs = []
81
- while source_info
82
- dirs << source_info.source_path if source_info.source_type == :directory
83
- source_info = source_info.parent
145
+ private
146
+
147
+ def search_array(search_dirs)
148
+ search_dirs.each do |search_dir|
149
+ result = search(search_dir)
150
+ return result if result
151
+ end
152
+ nil
153
+ end
154
+
155
+ def search_toys
156
+ source_info = @source_info
157
+ while source_info
158
+ if source_info.source_type == :directory
159
+ result = ::Toys::Utils::Gems.find_gemfile(source_info.source_path,
160
+ gemfile_names: @toys_gemfile_names)
161
+ return result if result
162
+ end
163
+ source_info = source_info.parent
164
+ end
165
+ nil
84
166
  end
85
- dirs
86
167
  end
87
168
  end
88
169
  end
@@ -728,7 +728,7 @@ module Toys
728
728
  def self._setup_clean_process(cmd)
729
729
  raise ::ArgumentError, "Toys process is unknown" unless ::Toys.executable_path
730
730
  cmd = ::Shellwords.split(cmd) if cmd.is_a?(::String)
731
- cmd = Array(::Toys.executable_path) + cmd
731
+ cmd = [::RbConfig.ruby, "--disable=gems", ::Toys.executable_path] + cmd
732
732
  if defined?(::Bundler)
733
733
  if ::Bundler.respond_to?(:with_unbundled_env)
734
734
  ::Bundler.with_unbundled_env { yield(cmd) }
@@ -547,7 +547,7 @@ module Toys
547
547
  ##
548
548
  # The process ID.
549
549
  #
550
- # Exactly one of `exception` and `pid` will be non-nil.
550
+ # Exactly one of {#exception} and {#pid} will be non-nil.
551
551
  #
552
552
  # @return [Integer] if the process start was successful
553
553
  # @return [nil] if the process could not be started.
@@ -557,7 +557,7 @@ module Toys
557
557
  ##
558
558
  # The exception raised when the process failed to start.
559
559
  #
560
- # Exactly one of `exception` and `pid` will be non-nil.
560
+ # Exactly one of {#exception} and {#pid} will be non-nil.
561
561
  #
562
562
  # @return [Exception] if the process failed to start.
563
563
  # @return [nil] if the process start was successful.
@@ -722,15 +722,20 @@ module Toys
722
722
  ##
723
723
  # Wait for the subcommand to complete, and return a result object.
724
724
  #
725
+ # Closes the control streams if present. The stdin stream is always
726
+ # closed, even if the call times out. The stdout and stderr streams are
727
+ # closed only after the command terminates.
728
+ #
725
729
  # @param timeout [Numeric,nil] The timeout in seconds, or `nil` to
726
730
  # wait indefinitely.
727
731
  # @return [Toys::Utils::Exec::Result] The result object
728
732
  # @return [nil] if a timeout occurred.
729
733
  #
730
734
  def result(timeout: nil)
735
+ close_streams(:in)
731
736
  return nil if @wait_thread && !@wait_thread.join(timeout)
732
737
  @result ||= begin
733
- close_streams
738
+ close_streams(:out)
734
739
  @join_threads.each(&:join)
735
740
  Result.new(name, @captures[:out], @captures[:err], @wait_thread&.value, @exception)
736
741
  .tap { |result| @result_callback&.call(result) }
@@ -738,13 +743,13 @@ module Toys
738
743
  end
739
744
 
740
745
  ##
741
- # Close all the controller's streams.
746
+ # Close the controller's streams.
742
747
  # @private
743
748
  #
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?
749
+ def close_streams(which)
750
+ @in.close if which != :out && @in && !@in.closed?
751
+ @out.close if which != :in && @out && !@out.closed?
752
+ @err.close if which != :in && @err && !@err.closed?
748
753
  self
749
754
  end
750
755
 
@@ -773,7 +778,21 @@ module Toys
773
778
  end
774
779
 
775
780
  ##
776
- # The result returned from a subcommand execution.
781
+ # The result returned from a subcommand execution. This includes the
782
+ # identifying name of the execution (if any), the result status of the
783
+ # execution, and any captured stream output.
784
+ #
785
+ # Possible result statuses are:
786
+ #
787
+ # * The process failed to start. {Result#failed?} will return true, and
788
+ # {Result#exception} will return an exception describing the failure
789
+ # (often an errno).
790
+ # * The process executed and exited with a normal exit code. Either
791
+ # {Result#success?} or {Result#error?} will return true, and
792
+ # {Result.exit_code} will return the numeric exit code.
793
+ # * The process executed but was terminated by an uncaught signal.
794
+ # {Result#signaled?} will return true, and {Result#term_signal} will
795
+ # return the numeric signal code.
777
796
  #
778
797
  class Result
779
798
  ## @private
@@ -809,11 +828,13 @@ module Toys
809
828
  attr_reader :captured_err
810
829
 
811
830
  ##
812
- # The status code object.
831
+ # The Ruby process status object, providing various information about
832
+ # the ending state of the process.
813
833
  #
814
- # Exactly one of `exception` and `status` will be non-nil.
834
+ # Exactly one of {#exception} and {#status} will be non-nil.
815
835
  #
816
- # @return [Process::Status] The status code.
836
+ # @return [Process::Status] The status, if the process was successfully
837
+ # spanwed and terminated.
817
838
  # @return [nil] if the process could not be started.
818
839
  #
819
840
  attr_reader :status
@@ -821,7 +842,7 @@ module Toys
821
842
  ##
822
843
  # The exception raised if a process couldn't be started.
823
844
  #
824
- # Exactly one of `exception` and `status` will be non-nil.
845
+ # Exactly one of {#exception} and {#status} will be non-nil.
825
846
  #
826
847
  # @return [Exception] The exception raised from process start.
827
848
  # @return [nil] if the process started successfully.
@@ -829,33 +850,76 @@ module Toys
829
850
  attr_reader :exception
830
851
 
831
852
  ##
832
- # The numeric status code.
853
+ # The numeric status code for a process that exited normally,
833
854
  #
834
- # This will be a nonzero integer if the process failed to start. That
835
- # is, `exit_code` will never be `nil`, even if `status` is `nil`.
855
+ # Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
856
+ # non-nil.
836
857
  #
837
- # @return [Integer]
858
+ # @return [Integer] the numeric status code, if the process started
859
+ # successfully and exited normally.
860
+ # @return [nil] if the process did not start successfully, or was
861
+ # terminated by an uncaught signal.
838
862
  #
839
863
  def exit_code
840
- status ? status.exitstatus : 127
864
+ status&.exitstatus
865
+ end
866
+
867
+ ##
868
+ # The numeric signal code that caused process termination.
869
+ #
870
+ # Exactly one of {#exception}, {#exit_code}, and {#term_signal} will be
871
+ # non-nil.
872
+ #
873
+ # @return [Integer] The signal that caused the process to terminate.
874
+ # @return [nil] if the process did not start successfully, or executed
875
+ # and exited with a normal exit code.
876
+ #
877
+ def term_signal
878
+ status&.termsig
879
+ end
880
+
881
+ ##
882
+ # Returns true if the subprocess failed to start, or false if the
883
+ # process was able to execute.
884
+ #
885
+ # @return [Boolean]
886
+ #
887
+ def failed?
888
+ status.nil?
889
+ end
890
+
891
+ ##
892
+ # Returns true if the subprocess terminated due to an unhandled signal,
893
+ # or false if the process failed to start or exited normally.
894
+ #
895
+ # @return [Boolean]
896
+ #
897
+ def signaled?
898
+ !term_signal.nil?
841
899
  end
842
900
 
843
901
  ##
844
- # Returns true if the subprocess terminated with a zero status.
902
+ # Returns true if the subprocess terminated with a zero status, or
903
+ # false if the process failed to start, terminated due to a signal, or
904
+ # returned a nonzero status.
845
905
  #
846
906
  # @return [Boolean]
847
907
  #
848
908
  def success?
849
- exit_code.zero?
909
+ code = exit_code
910
+ !code.nil? && code.zero?
850
911
  end
851
912
 
852
913
  ##
853
- # Returns true if the subprocess terminated with a nonzero status.
914
+ # Returns true if the subprocess terminated with a nonzero status, or
915
+ # false if the process failed to start, terminated due to a signal, or
916
+ # returned a zero status.
854
917
  #
855
918
  # @return [Boolean]
856
919
  #
857
920
  def error?
858
- !exit_code.zero?
921
+ code = exit_code
922
+ !code.nil? && !code.zero?
859
923
  end
860
924
  end
861
925
 
@@ -887,10 +951,10 @@ module Toys
887
951
  return controller if @config_opts[:background]
888
952
  begin
889
953
  @block&.call(controller)
954
+ controller.result
890
955
  ensure
891
- controller.close_streams
956
+ controller.close_streams(:both)
892
957
  end
893
- controller.result
894
958
  end
895
959
 
896
960
  private
@@ -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).
@@ -81,16 +94,22 @@ module Toys
81
94
  #
82
95
  # @param on_missing [:confirm,:error,:install] What to do if a needed gem
83
96
  # is not installed. Possible values:
97
+ #
84
98
  # * `:confirm` - prompt the user on whether to install
85
99
  # * `:error` - raise an exception
86
100
  # * `:install` - just install the gem
101
+ #
87
102
  # The default is `:confirm`.
103
+ #
88
104
  # @param on_conflict [:error,:warn,:ignore] What to do if bundler has
89
105
  # already been run with a different Gemfile. Possible values:
106
+ #
90
107
  # * `:error` - raise an exception
91
108
  # * `:ignore` - just silently proceed without bundling again
92
109
  # * `:warn` - print a warning and proceed without bundling again
110
+ #
93
111
  # The default is `:error`.
112
+ #
94
113
  # @param terminal [Toys::Utils::Terminal] Terminal to use (optional)
95
114
  # @param input [IO] Input IO (optional, defaults to STDIN)
96
115
  # @param output [IO] Output IO (optional, defaults to STDOUT)
@@ -136,26 +155,54 @@ module Toys
136
155
  end
137
156
 
138
157
  ##
139
- # Set up the bundle.
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}.
140
172
  #
141
- # @param groups [Array<String>] The groups to include in setup
142
- # @param search_dirs [Array<String>] Directories to search for a Gemfile
143
173
  # @return [void]
144
174
  #
145
175
  def bundle(groups: nil,
146
- search_dirs: nil)
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
147
184
  Gems.synchronize do
148
- gemfile_path = find_gemfile(Array(search_dirs))
149
- activate("bundler", "~> 2.1")
150
185
  if configure_gemfile(gemfile_path)
186
+ activate("bundler", "~> 2.1")
187
+ require "bundler"
151
188
  setup_bundle(gemfile_path, groups || [])
152
189
  end
153
190
  end
154
191
  end
155
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
+
156
203
  @global_mutex = ::Monitor.new
157
204
 
158
- ## @private
205
+ # @private
159
206
  def self.synchronize(&block)
160
207
  @global_mutex.synchronize(&block)
161
208
  end
@@ -184,14 +231,14 @@ module Toys
184
231
  error.message.include?("Could not find")
185
232
  end
186
233
  if !is_missing_spec || @on_missing == :error
187
- report_error(name, requirements, error)
234
+ report_activation_error(name, requirements, error)
188
235
  return
189
236
  end
190
237
  confirm_and_install_gem(name, requirements)
191
238
  begin
192
239
  gem(name, *requirements)
193
240
  rescue ::Gem::LoadError => e
194
- report_error(name, requirements, e)
241
+ report_activation_error(name, requirements, e)
195
242
  end
196
243
  end
197
244
 
@@ -215,7 +262,7 @@ module Toys
215
262
  ::Gem::Specification.reset
216
263
  end
217
264
 
218
- def report_error(name, requirements, err)
265
+ def report_activation_error(name, requirements, err)
219
266
  if ::ENV["BUNDLE_GEMFILE"]
220
267
  raise GemfileUpdateNeededError.new(gem_requirements_text(name, requirements),
221
268
  ::ENV["BUNDLE_GEMFILE"])
@@ -223,22 +270,16 @@ module Toys
223
270
  raise ActivationFailedError, err.message
224
271
  end
225
272
 
226
- def find_gemfile(search_dirs)
227
- search_dirs.each do |dir|
228
- gemfile_path = ::File.join(dir, "Gemfile")
229
- return gemfile_path if ::File.readable?(gemfile_path)
230
- end
231
- raise GemfileNotFoundError, "Gemfile not found"
232
- end
233
-
234
273
  def configure_gemfile(gemfile_path)
235
274
  old_path = ::ENV["BUNDLE_GEMFILE"]
236
- if old_path && gemfile_path != old_path
237
- case @on_conflict
238
- when :warn
239
- terminal.puts("Warning: could not set up bundler because it is already set up.", :red)
240
- when :error
241
- raise AlreadyBundledError, "Could not set up bundler because it is already set up"
275
+ if old_path
276
+ if gemfile_path != old_path
277
+ case @on_conflict
278
+ when :warn
279
+ terminal.puts("Warning: could not set up bundler because it is already set up.", :red)
280
+ when :error
281
+ raise AlreadyBundledError, "Could not set up bundler because it is already set up"
282
+ end
242
283
  end
243
284
  return false
244
285
  end
@@ -247,13 +288,60 @@ module Toys
247
288
  end
248
289
 
249
290
  def setup_bundle(gemfile_path, groups)
250
- require "bundler"
251
291
  begin
252
- ::Bundler.setup(*groups)
253
- rescue ::Bundler::GemNotFound
292
+ modify_bundle_definition(gemfile_path)
293
+ ::Bundler.ui.silence { ::Bundler.setup(*groups) }
294
+ rescue ::Bundler::GemNotFound, ::Bundler::VersionConflict
295
+ restore_toys_libs
254
296
  install_bundle(gemfile_path)
255
297
  ::Bundler.reset!
256
- ::Bundler.setup(*groups)
298
+ modify_bundle_definition(gemfile_path)
299
+ ::Bundler.ui.silence { ::Bundler.setup(*groups) }
300
+ end
301
+ restore_toys_libs
302
+ end
303
+
304
+ def modify_bundle_definition(gemfile_path)
305
+ builder = ::Bundler::Dsl.new
306
+ builder.eval_gemfile(gemfile_path)
307
+ toys_gems = ["toys-core"]
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
315
+ definition = builder.to_definition(gemfile_path + ".lock", { gems: toys_gems })
316
+ ::Bundler.instance_variable_set(:@definition, definition)
317
+ end
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
+
339
+ def restore_toys_libs
340
+ $LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
341
+ $LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
342
+ if ::Toys.const_defined?(:LIB_PATH)
343
+ $LOAD_PATH.delete(::Toys::LIB_PATH)
344
+ $LOAD_PATH.unshift(::Toys::LIB_PATH)
257
345
  end
258
346
  end
259
347
 
@@ -264,7 +352,8 @@ module Toys
264
352
  when :error
265
353
  false
266
354
  else
267
- terminal.confirm("Your bundle is not complete. Install? ", default: @default_confirm)
355
+ terminal.confirm("Your bundle requires additional gems. Install? ",
356
+ default: @default_confirm)
268
357
  end
269
358
  end
270
359
 
@@ -276,7 +365,12 @@ module Toys
276
365
  " `cd #{gemfile_dir} && bundle install`"
277
366
  end
278
367
  require "bundler/cli"
279
- ::Bundler::CLI.start(["install"])
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
280
374
  end
281
375
  end
282
376
  end