toys-core 0.10.2 → 0.11.1

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.
@@ -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,21 +288,61 @@ 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
254
295
  restore_toys_libs
255
296
  install_bundle(gemfile_path)
256
297
  ::Bundler.reset!
257
- ::Bundler.setup(*groups)
298
+ modify_bundle_definition(gemfile_path)
299
+ ::Bundler.ui.silence { ::Bundler.setup(*groups) }
258
300
  end
259
301
  restore_toys_libs
260
302
  end
261
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
+
262
339
  def restore_toys_libs
340
+ $LOAD_PATH.delete(::Toys::CORE_LIB_PATH)
263
341
  $LOAD_PATH.unshift(::Toys::CORE_LIB_PATH)
264
- $LOAD_PATH.unshift(::Toys::LIB_PATH) if ::Toys.const_defined?(:LIB_PATH)
342
+ if ::Toys.const_defined?(:LIB_PATH)
343
+ $LOAD_PATH.delete(::Toys::LIB_PATH)
344
+ $LOAD_PATH.unshift(::Toys::LIB_PATH)
345
+ end
265
346
  end
266
347
 
267
348
  def permission_to_bundle?
@@ -271,7 +352,8 @@ module Toys
271
352
  when :error
272
353
  false
273
354
  else
274
- terminal.confirm("Your bundle is not complete. Install? ", default: @default_confirm)
355
+ terminal.confirm("Your bundle requires additional gems. Install? ",
356
+ default: @default_confirm)
275
357
  end
276
358
  end
277
359
 
@@ -283,7 +365,12 @@ module Toys
283
365
  " `cd #{gemfile_dir} && bundle install`"
284
366
  end
285
367
  require "bundler/cli"
286
- ::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
287
374
  end
288
375
  end
289
376
  end
@@ -32,14 +32,13 @@ module Toys
32
32
  # @return [Toys::Utils::HelpText]
33
33
  #
34
34
  def self.from_context(context)
35
- orig_context = context
36
- while (from = context[Context::Key::DELEGATED_FROM])
37
- context = from
35
+ delegates = []
36
+ cur = context
37
+ while (cur = cur[Context::Key::DELEGATED_FROM])
38
+ delegates << cur[Context::Key::TOOL]
38
39
  end
39
- delegate_target = orig_context == context ? nil : orig_context[Context::Key::TOOL_NAME]
40
40
  cli = context[Context::Key::CLI]
41
- new(context[Context::Key::TOOL], cli.loader, cli.executable_name,
42
- delegate_target: delegate_target)
41
+ new(context[Context::Key::TOOL], cli.loader, cli.executable_name, delegates: delegates)
43
42
  end
44
43
 
45
44
  ##
@@ -49,16 +48,15 @@ module Toys
49
48
  # @param loader [Toys::Loader] A loader that can provide subcommands.
50
49
  # @param executable_name [String] The name of the executable.
51
50
  # e.g. `"toys"`.
52
- # @param delegate_target [Array<String>,nil] The full name of a tool this
53
- # tool will delegate to. Default is `nil` for no delegation.
51
+ # @param delegates [Array<Toys::Tool>] The delegation path to the tool.
54
52
  #
55
53
  # @return [Toys::Utils::HelpText]
56
54
  #
57
- def initialize(tool, loader, executable_name, delegate_target: nil)
55
+ def initialize(tool, loader, executable_name, delegates: [])
58
56
  @tool = tool
59
57
  @loader = loader
60
58
  @executable_name = executable_name
61
- @delegate_target = delegate_target
59
+ @delegates = delegates
62
60
  end
63
61
 
64
62
  ##
@@ -88,8 +86,7 @@ module Toys
88
86
  indent ||= DEFAULT_INDENT
89
87
  subtools = find_subtools(recursive, nil, include_hidden)
90
88
  assembler = UsageStringAssembler.new(
91
- @tool, @executable_name, @delegate_target, subtools,
92
- indent, left_column_width, wrap_width
89
+ @tool, @executable_name, subtools, indent, left_column_width, wrap_width
93
90
  )
94
91
  assembler.result
95
92
  end
@@ -121,7 +118,7 @@ module Toys
121
118
  indent2 ||= DEFAULT_INDENT
122
119
  subtools = find_subtools(recursive, search, include_hidden)
123
120
  assembler = HelpStringAssembler.new(
124
- @tool, @executable_name, @delegate_target, subtools, search, show_source_path,
121
+ @tool, @executable_name, @delegates, subtools, search, show_source_path,
125
122
  indent, indent2, wrap_width, styled
126
123
  )
127
124
  assembler.result
@@ -155,22 +152,29 @@ module Toys
155
152
  private
156
153
 
157
154
  def find_subtools(recursive, search, include_hidden)
158
- subtools = @loader.list_subtools(@tool.full_name,
159
- recursive: recursive, include_hidden: include_hidden)
160
- return subtools if search.nil? || search.empty?
155
+ subtools_by_name = {}
156
+ ([@tool] + @delegates).each do |tool|
157
+ name_len = tool.full_name.length
158
+ subtools = @loader.list_subtools(tool.full_name,
159
+ recursive: recursive, include_hidden: include_hidden)
160
+ subtools.each do |subtool|
161
+ local_name = subtool.full_name.slice(name_len..-1).join(" ")
162
+ subtools_by_name[local_name] = subtool
163
+ end
164
+ end
165
+ subtool_list = subtools_by_name.sort_by { |(local_name, _tool)| local_name }
166
+ return subtool_list if search.nil? || search.empty?
161
167
  regex = ::Regexp.new(search, ::Regexp::IGNORECASE)
162
- subtools.find_all do |tool|
163
- regex =~ tool.display_name || regex =~ tool.desc.to_s
168
+ subtool_list.find_all do |local_name, tool|
169
+ regex =~ local_name || regex =~ tool.desc.to_s
164
170
  end
165
171
  end
166
172
 
167
173
  ## @private
168
174
  class UsageStringAssembler
169
- def initialize(tool, executable_name, delegate_target, subtools,
170
- indent, left_column_width, wrap_width)
175
+ def initialize(tool, executable_name, subtools, indent, left_column_width, wrap_width)
171
176
  @tool = tool
172
177
  @executable_name = executable_name
173
- @delegate_target = delegate_target
174
178
  @subtools = subtools
175
179
  @indent = indent
176
180
  @left_column_width = left_column_width
@@ -195,7 +199,7 @@ module Toys
195
199
  def add_synopsis_section
196
200
  synopses = []
197
201
  synopses << namespace_synopsis unless @subtools.empty?
198
- synopses << (@delegate_target ? delegate_synopsis : tool_synopsis)
202
+ synopses << tool_synopsis
199
203
  first = true
200
204
  synopses.each do |synopsis|
201
205
  @lines << (first ? "Usage: #{synopsis}" : " #{synopsis}")
@@ -212,11 +216,6 @@ module Toys
212
216
  synopsis.join(" ")
213
217
  end
214
218
 
215
- def delegate_synopsis
216
- target = @delegate_target.join(" ")
217
- "#{@executable_name} #{@tool.display_name} [ARGUMENTS FOR \"#{target}\"...]"
218
- end
219
-
220
219
  def namespace_synopsis
221
220
  "#{@executable_name} #{@tool.display_name} TOOL [ARGUMENTS...]"
222
221
  end
@@ -256,12 +255,10 @@ module Toys
256
255
 
257
256
  def add_subtool_list_section
258
257
  return if @subtools.empty?
259
- name_len = @tool.full_name.length
260
258
  @lines << ""
261
259
  @lines << "Tools:"
262
- @subtools.each do |subtool|
263
- tool_name = subtool.full_name.slice(name_len..-1).join(" ")
264
- add_right_column_desc(tool_name, wrap_desc(subtool.desc))
260
+ @subtools.each do |local_name, subtool|
261
+ add_right_column_desc(local_name, wrap_desc(subtool.desc))
265
262
  end
266
263
  end
267
264
 
@@ -301,12 +298,12 @@ module Toys
301
298
 
302
299
  ## @private
303
300
  class HelpStringAssembler
304
- def initialize(tool, executable_name, delegate_target, subtools, search_term,
301
+ def initialize(tool, executable_name, delegates, subtools, search_term,
305
302
  show_source_path, indent, indent2, wrap_width, styled)
306
303
  require "toys/utils/terminal"
307
304
  @tool = tool
308
305
  @executable_name = executable_name
309
- @delegate_target = delegate_target
306
+ @delegates = delegates
310
307
  @subtools = subtools
311
308
  @search_term = search_term
312
309
  @show_source_path = show_source_path
@@ -356,7 +353,7 @@ module Toys
356
353
  @lines << ""
357
354
  @lines << bold("SYNOPSIS")
358
355
  add_synopsis_clause(namespace_synopsis) unless @subtools.empty?
359
- add_synopsis_clause(@delegate_target ? delegate_synopsis : tool_synopsis)
356
+ add_synopsis_clause(tool_synopsis(@tool))
360
357
  end
361
358
 
362
359
  def add_synopsis_clause(synopsis)
@@ -367,8 +364,8 @@ module Toys
367
364
  end
368
365
  end
369
366
 
370
- def tool_synopsis
371
- synopsis = [full_executable_name]
367
+ def tool_synopsis(tool_for_name)
368
+ synopsis = [full_executable_name(tool_for_name)]
372
369
  @tool.flag_groups.each do |flag_group|
373
370
  case flag_group
374
371
  when FlagGroup::Required
@@ -441,19 +438,14 @@ module Toys
441
438
  end
442
439
 
443
440
  def namespace_synopsis
444
- synopsis = [full_executable_name, underline("TOOL"), "[#{underline('ARGUMENTS')}...]"]
441
+ synopsis = [full_executable_name(@tool),
442
+ underline("TOOL"),
443
+ "[#{underline('ARGUMENTS')}...]"]
445
444
  wrap_indent_indent2(WrappableString.new(synopsis))
446
445
  end
447
446
 
448
- def delegate_synopsis
449
- target = @delegate_target.join(" ")
450
- args_clause = underline("ARGUMENTS FOR \"#{target}\"")
451
- synopsis = [full_executable_name, "[#{args_clause}...]"]
452
- wrap_indent_indent2(WrappableString.new(synopsis))
453
- end
454
-
455
- def full_executable_name
456
- bold(([@executable_name] + @tool.full_name).join(" "))
447
+ def full_executable_name(tool_for_name)
448
+ bold(([@executable_name] + tool_for_name.full_name).join(" "))
457
449
  end
458
450
 
459
451
  def add_source_section
@@ -461,15 +453,22 @@ module Toys
461
453
  @lines << ""
462
454
  @lines << bold("SOURCE")
463
455
  @lines << indent_str("Defined in #{@tool.source_info.source_name}")
456
+ @delegates.each do |delegate|
457
+ @lines << indent_str("Delegated from \"#{delegate.display_name}\"" \
458
+ " defined in #{delegate.source_info.source_name}")
459
+ end
464
460
  end
465
461
 
466
462
  def add_description_section
467
- desc = @tool.long_desc
468
- if @delegate_target
469
- delegate_clause =
470
- "Passes all arguments to \"#{@delegate_target.join(' ')}\" if invoked directly."
471
- desc = desc.empty? ? [delegate_clause] : desc + ["", delegate_clause]
463
+ desc = @tool.long_desc.dup
464
+ @delegates.each do |delegate|
465
+ desc << "" << "Delegated from \"#{delegate.display_name}\""
466
+ unless delegate.long_desc.empty?
467
+ desc << ""
468
+ desc += delegate.long_desc
469
+ end
472
470
  end
471
+ desc = desc[1..-1] if desc.first == ""
473
472
  desc = wrap_indent(desc)
474
473
  return if desc.empty?
475
474
  @lines << ""
@@ -533,10 +532,8 @@ module Toys
533
532
  @lines << indent_str("Showing search results for \"#{@search_term}\"")
534
533
  @lines << ""
535
534
  end
536
- name_len = @tool.full_name.length
537
- @subtools.each do |subtool|
538
- tool_name = subtool.full_name.slice(name_len..-1).join(" ")
539
- add_prefix_with_desc(bold(tool_name), subtool.desc)
535
+ @subtools.each do |local_name, subtool|
536
+ add_prefix_with_desc(bold(local_name), subtool.desc)
540
537
  end
541
538
  end
542
539
 
@@ -637,10 +634,8 @@ module Toys
637
634
  end
638
635
 
639
636
  def add_list
640
- name_len = @tool.full_name.length
641
- @subtools.each do |subtool|
642
- tool_name = subtool.full_name.slice(name_len..-1).join(" ")
643
- add_prefix_with_desc(bold(tool_name), subtool.desc)
637
+ @subtools.each do |local_name, subtool|
638
+ add_prefix_with_desc(bold(local_name), subtool.desc)
644
639
  end
645
640
  end
646
641