toys-core 0.10.4 → 0.11.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,9 @@ module Toys
8
8
  # appropriate tool given a set of command line arguments.
9
9
  #
10
10
  class Loader
11
+ # @private
12
+ BASE_PRIORITY = -999_999
13
+
11
14
  ##
12
15
  # Create a Loader
13
16
  #
@@ -69,9 +72,11 @@ module Toys
69
72
  @worklist = []
70
73
  @tool_data = {}
71
74
  @max_priority = @min_priority = 0
75
+ @stop_priority = BASE_PRIORITY
76
+ @min_loaded_priority = 999_999
72
77
  @middleware_stack = Middleware.stack(middleware_stack)
73
78
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
74
- get_tool([], -999_999)
79
+ get_tool([], BASE_PRIORITY)
75
80
  end
76
81
 
77
82
  ##
@@ -89,7 +94,7 @@ module Toys
89
94
  raise "Cannot add a path after tool loading has started" if @loading_started
90
95
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
91
96
  paths.each do |path|
92
- source = SourceInfo.create_path_root(path)
97
+ source = SourceInfo.create_path_root(path, @data_dir_name, @lib_dir_name)
93
98
  @worklist << [source, [], priority]
94
99
  end
95
100
  end
@@ -114,7 +119,7 @@ module Toys
114
119
  @mutex.synchronize do
115
120
  raise "Cannot add a block after tool loading has started" if @loading_started
116
121
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
117
- source = SourceInfo.create_proc_root(block, name)
122
+ source = SourceInfo.create_proc_root(block, name, @data_dir_name, @lib_dir_name)
118
123
  @worklist << [source, [], priority]
119
124
  end
120
125
  self
@@ -178,15 +183,15 @@ module Toys
178
183
  load_for_prefix(words)
179
184
  found_tools = []
180
185
  len = words.length
181
- tool_data_snapshot.each do |n, td|
182
- next if n.empty?
186
+ all_cur_definitions.each do |tool|
187
+ name = tool.full_name
188
+ next if name.empty?
183
189
  if recursive
184
- next if n.length <= len || n.slice(0, len) != words
190
+ next if name.length <= len || name.slice(0, len) != words
185
191
  else
186
- next unless n.slice(0..-2) == words
192
+ next unless name.slice(0..-2) == words
187
193
  end
188
- tool = td.cur_definition
189
- found_tools << tool unless tool.nil?
194
+ found_tools << tool
190
195
  end
191
196
  sort_tools_by_name(found_tools)
192
197
  include_hidden ? found_tools : filter_hidden_subtools(found_tools)
@@ -202,8 +207,9 @@ module Toys
202
207
  def has_subtools?(words) # rubocop:disable Naming/PredicateName
203
208
  load_for_prefix(words)
204
209
  len = words.length
205
- tool_data_snapshot.each do |n, td|
206
- if !n.empty? && n.length > len && n.slice(0, len) == words && !td.empty?
210
+ all_cur_definitions.each do |tool|
211
+ name = tool.full_name
212
+ if !name.empty? && name.length > len && name.slice(0, len) == words
207
213
  return true
208
214
  end
209
215
  end
@@ -267,6 +273,20 @@ module Toys
267
273
  Tool.new(self, parent, words, priority, middleware_stack, @middleware_lookup)
268
274
  end
269
275
 
276
+ ##
277
+ # Stop search at the given priority. Returns true if successful.
278
+ # Called only from the DSL.
279
+ #
280
+ # @private
281
+ #
282
+ def stop_loading_at_priority(priority)
283
+ @mutex.synchronize do
284
+ return false if priority > @min_loaded_priority || priority < @stop_priority
285
+ @stop_priority = priority
286
+ true
287
+ end
288
+ end
289
+
270
290
  ##
271
291
  # Loads the subtree under the given prefix.
272
292
  #
@@ -278,6 +298,7 @@ module Toys
278
298
  cur_worklist = @worklist
279
299
  @worklist = []
280
300
  cur_worklist.each do |source, words, priority|
301
+ next if priority < @stop_priority
281
302
  remaining_words = calc_remaining_words(prefix, words)
282
303
  if source.source_proc
283
304
  load_proc(source, words, remaining_words, priority)
@@ -349,8 +370,15 @@ module Toys
349
370
 
350
371
  private
351
372
 
352
- def tool_data_snapshot
353
- @mutex.synchronize { @tool_data.dup }
373
+ def all_cur_definitions
374
+ result = []
375
+ @mutex.synchronize do
376
+ @tool_data.map do |_name, td|
377
+ tool = td.cur_definition
378
+ result << tool unless tool.nil?
379
+ end
380
+ end
381
+ result
354
382
  end
355
383
 
356
384
  def get_tool_data(words)
@@ -364,14 +392,16 @@ module Toys
364
392
  def finish_definitions_in_tree(words)
365
393
  load_for_prefix(words)
366
394
  len = words.length
367
- tool_data_snapshot.each do |n, td|
368
- next if n.length < len || n.slice(0, len) != words
369
- td.cur_definition&.finish_definition(self)
395
+ all_cur_definitions.each do |tool|
396
+ name = tool.full_name
397
+ next if name.length < len || name.slice(0, len) != words
398
+ tool.finish_definition(self)
370
399
  end
371
400
  end
372
401
 
373
402
  def load_proc(source, words, remaining_words, priority)
374
403
  if remaining_words
404
+ update_min_loaded_priority(priority)
375
405
  tool_class = get_tool(words, priority).tool_class
376
406
  DSL::Tool.prepare(tool_class, remaining_words, source) do
377
407
  ContextualError.capture("Error while loading Toys config!") do
@@ -393,6 +423,7 @@ module Toys
393
423
 
394
424
  def load_relevant_path(source, words, remaining_words, priority)
395
425
  if source.source_type == :file
426
+ update_min_loaded_priority(priority)
396
427
  tool_class = get_tool(words, priority).tool_class
397
428
  InputFile.evaluate(tool_class, remaining_words, source)
398
429
  else
@@ -406,7 +437,7 @@ module Toys
406
437
 
407
438
  def load_index_in(source, words, remaining_words, priority)
408
439
  return unless @index_file_name
409
- index_source = source.relative_child(@index_file_name, @data_dir_name, @lib_dir_name)
440
+ index_source = source.relative_child(@index_file_name)
410
441
  load_relevant_path(index_source, words, remaining_words, priority) if index_source
411
442
  end
412
443
 
@@ -414,7 +445,7 @@ module Toys
414
445
  return if child.start_with?(".") || child == @index_file_name ||
415
446
  child == @preload_file_name || child == @preload_dir_name ||
416
447
  child == @data_dir_name || child == @lib_dir_name
417
- child_source = source.relative_child(child, @data_dir_name, @lib_dir_name)
448
+ child_source = source.relative_child(child)
418
449
  return unless child_source
419
450
  child_word = ::File.basename(child, ".rb")
420
451
  next_words = words + [child_word]
@@ -422,6 +453,10 @@ module Toys
422
453
  load_validated_path(child_source, next_words, next_remaining, priority)
423
454
  end
424
455
 
456
+ def update_min_loaded_priority(priority)
457
+ @min_loaded_priority = priority if @min_loaded_priority > priority
458
+ end
459
+
425
460
  def do_preload(path)
426
461
  if @preload_file_name
427
462
  preload_file = ::File.join(path, @preload_file_name)
@@ -483,37 +518,42 @@ module Toys
483
518
  # @private
484
519
  #
485
520
  class ToolData
486
- ## @private
521
+ # @private
487
522
  def initialize(words)
488
523
  @words = words
489
524
  @definitions = {}
490
525
  @top_priority = @active_priority = nil
526
+ @mutex = ::Monitor.new
491
527
  end
492
528
 
493
- ## @private
529
+ # @private
494
530
  def cur_definition
495
- active_definition || top_definition
531
+ @mutex.synchronize { active_definition || top_definition }
496
532
  end
497
533
 
498
- ## @private
534
+ # @private
499
535
  def empty?
500
536
  @definitions.empty?
501
537
  end
502
538
 
503
- ## @private
539
+ # @private
504
540
  def get_tool(priority, loader)
505
- if @top_priority.nil? || @top_priority < priority
506
- @top_priority = priority
541
+ @mutex.synchronize do
542
+ if @top_priority.nil? || @top_priority < priority
543
+ @top_priority = priority
544
+ end
545
+ @definitions[priority] ||= loader.build_tool(@words, priority)
507
546
  end
508
- @definitions[priority] ||= loader.build_tool(@words, priority)
509
547
  end
510
548
 
511
- ## @private
549
+ # @private
512
550
  def activate_tool(priority, loader)
513
- return active_definition if @active_priority == priority
514
- return nil if @active_priority && @active_priority > priority
515
- @active_priority = priority
516
- get_tool(priority, loader)
551
+ @mutex.synchronize do
552
+ return active_definition if @active_priority == priority
553
+ return nil if @active_priority && @active_priority > priority
554
+ @active_priority = priority
555
+ get_tool(priority, loader)
556
+ end
517
557
  end
518
558
 
519
559
  private
@@ -10,7 +10,7 @@ module Toys
10
10
  # @private
11
11
  #
12
12
  def initialize(parent, context_directory, source_type, source_path, source_proc,
13
- source_name, data_dir, lib_dir)
13
+ source_name, data_dir_name, lib_dir_name)
14
14
  @parent = parent
15
15
  @context_directory = context_directory
16
16
  @source_type = source_type
@@ -18,8 +18,10 @@ module Toys
18
18
  @source_path = source_path
19
19
  @source_proc = source_proc
20
20
  @source_name = source_name
21
- @data_dir = data_dir
22
- @lib_dir = lib_dir
21
+ @data_dir_name = data_dir_name
22
+ @lib_dir_name = lib_dir_name
23
+ @data_dir = find_special_dir(data_dir_name)
24
+ @lib_dir = find_special_dir(lib_dir_name)
23
25
  end
24
26
 
25
27
  ##
@@ -118,15 +120,13 @@ module Toys
118
120
  # Create a child SourceInfo relative to the parent path.
119
121
  # @private
120
122
  #
121
- def relative_child(filename, data_dir_name, lib_dir_name)
122
- raise "no parent path for relative_child" unless source_path
123
+ def relative_child(filename)
124
+ raise "relative_child is valid only on a directory source" unless source_type == :directory
123
125
  child_path = ::File.join(source_path, filename)
124
126
  child_path, type = SourceInfo.check_path(child_path, true)
125
127
  return nil unless child_path
126
- data_dir = SourceInfo.find_special_dir(type, child_path, data_dir_name)
127
- lib_dir = SourceInfo.find_special_dir(type, child_path, lib_dir_name)
128
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path,
129
- data_dir, lib_dir)
128
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
129
+ @data_dir_name, @lib_dir_name)
130
130
  end
131
131
 
132
132
  ##
@@ -135,7 +135,8 @@ module Toys
135
135
  #
136
136
  def absolute_child(child_path)
137
137
  child_path, type = SourceInfo.check_path(child_path, false)
138
- SourceInfo.new(self, context_directory, type, child_path, source_proc, child_path, nil, nil)
138
+ SourceInfo.new(self, context_directory, type, child_path, nil, child_path,
139
+ @data_dir_name, @lib_dir_name)
139
140
  end
140
141
 
141
142
  ##
@@ -144,25 +145,26 @@ module Toys
144
145
  #
145
146
  def proc_child(child_proc, source_name = nil)
146
147
  source_name ||= self.source_name
147
- SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name, nil, nil)
148
+ SourceInfo.new(self, context_directory, :proc, source_path, child_proc, source_name,
149
+ @data_dir_name, @lib_dir_name)
148
150
  end
149
151
 
150
152
  ##
151
153
  # Create a root source info for a file path.
152
154
  # @private
153
155
  #
154
- def self.create_path_root(source_path)
156
+ def self.create_path_root(source_path, data_dir_name, lib_dir_name)
155
157
  source_path, type = check_path(source_path, false)
156
158
  context_directory = ::File.dirname(source_path)
157
- new(nil, context_directory, type, source_path, nil, source_path, nil, nil)
159
+ new(nil, context_directory, type, source_path, nil, source_path, data_dir_name, lib_dir_name)
158
160
  end
159
161
 
160
162
  ##
161
163
  # Create a root source info for a proc.
162
164
  # @private
163
165
  #
164
- def self.create_proc_root(source_proc, source_name)
165
- new(nil, nil, :proc, nil, source_proc, source_name, nil, nil)
166
+ def self.create_proc_root(source_proc, source_name, data_dir_name, lib_dir_name)
167
+ new(nil, nil, :proc, nil, source_proc, source_name, data_dir_name, lib_dir_name)
166
168
  end
167
169
 
168
170
  ##
@@ -189,14 +191,11 @@ module Toys
189
191
  end
190
192
  end
191
193
 
192
- ##
193
- # Determine the data directory path, if any.
194
- # @private
195
- #
196
- def self.find_special_dir(type, source_path, dir_name)
197
- return nil if source_path.nil? || dir_name.nil?
198
- source_path = ::File.dirname(source_path) if type == :file
199
- dir = ::File.join(source_path, dir_name)
194
+ private
195
+
196
+ def find_special_dir(dir_name)
197
+ return nil if @source_type != :directory || dir_name.nil?
198
+ dir = ::File.join(@source_path, dir_name)
200
199
  dir if ::File.directory?(dir) && ::File.readable?(dir)
201
200
  end
202
201
  end
@@ -11,32 +11,51 @@ module Toys
11
11
  # defining the tool. If `false` (the default), installs the bundle just
12
12
  # before the tool runs.
13
13
  #
14
- # * `:groups` (Array<String>) The groups to include in setup
14
+ # * `:groups` (Array\<String\>) The groups to include in setup.
15
15
  #
16
- # * `:search_dirs` (Array<String,Symbol>) Directories to search for a
17
- # Gemfile.
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.
18
21
  #
19
22
  # You can pass full directory paths, and/or any of the following:
20
- # * `:context` - the current context directory
21
- # * `:current` - the current working directory
22
- # * `: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.
23
27
  #
24
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}.
25
44
  #
26
45
  # * `:on_missing` (Symbol) What to do if a needed gem is not installed.
27
46
  #
28
47
  # Supported values:
29
- # * `:confirm` - prompt the user on whether to install (default)
30
- # * `:error` - raise an exception
31
- # * `: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.
32
51
  #
33
52
  # * `:on_conflict` (Symbol) What to do if bundler has already been run
34
53
  # with a different Gemfile.
35
54
  #
36
55
  # Supported values:
37
- # * `:error` - raise an exception (default)
38
- # * `:ignore` - just silently proceed without bundling again
39
- # * `: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.
40
59
  #
41
60
  # * `:terminal` (Toys::Utils::Terminal) Terminal to use (optional)
42
61
  # * `:input` (IO) Input IO (optional, defaults to STDIN)
@@ -45,68 +64,106 @@ module Toys
45
64
  module Bundler
46
65
  include Mixin
47
66
 
48
- on_initialize do |static: false, search_dirs: nil, **kwargs|
67
+ on_initialize do |static: false, **kwargs|
49
68
  unless static
50
- require "toys/utils/gems"
51
- search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(
52
- search_dirs,
53
- self[::Toys::Context::Key::CONTEXT_DIRECTORY],
54
- self[::Toys::Context::Key::TOOL_SOURCE]
55
- )
56
- ::Toys::StandardMixins::Bundler.setup_bundle(search_dirs, **kwargs)
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)
57
72
  end
58
73
  end
59
74
 
60
- on_include do |static: false, search_dirs: nil, **kwargs|
75
+ on_include do |static: false, **kwargs|
61
76
  if static
62
- require "toys/utils/gems"
63
- search_dirs = ::Toys::StandardMixins::Bundler.resolve_search_dirs(
64
- search_dirs, context_directory, source_info
65
- )
66
- ::Toys::StandardMixins::Bundler.setup_bundle(search_dirs, **kwargs)
77
+ ::Toys::StandardMixins::Bundler.setup_bundle(context_directory, source_info, **kwargs)
67
78
  end
68
79
  end
69
80
 
70
- ## @private
71
- def self.resolve_search_dirs(search_dirs, context_dir, source_info)
72
- search_dirs ||= [:toys, :context, :current]
73
- Array(search_dirs).flat_map do |dir|
74
- case dir
75
- when :context
76
- context_dir
77
- when :current
78
- ::Dir.getwd
79
- when :toys
80
- toys_dir_stack(source_info)
81
- when ::String
82
- dir
83
- else
84
- raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
85
- end
86
- end
87
- end
81
+ ##
82
+ # Default search directories for Gemfiles.
83
+ # @return [Array<String,Symbol>]
84
+ #
85
+ DEFAULT_SEARCH_DIRS = [:toys, :context, :current].freeze
88
86
 
89
- ## @private
90
- def self.toys_dir_stack(source_info)
91
- dirs = []
92
- while source_info
93
- dirs << source_info.source_path if source_info.source_type == :directory
94
- source_info = source_info.parent
95
- end
96
- dirs
97
- end
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
98
92
 
99
- ## @private
100
- def self.setup_bundle(search_dirs,
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,
101
100
  groups: nil,
102
101
  on_missing: nil,
103
102
  on_conflict: nil,
104
103
  terminal: nil,
105
104
  input: nil,
106
105
  output: nil)
106
+ require "toys/utils/gems"
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
107
112
  gems = ::Toys::Utils::Gems.new(on_missing: on_missing, on_conflict: on_conflict,
108
113
  terminal: terminal, input: input, output: output)
109
- gems.bundle(groups: groups, search_dirs: search_dirs)
114
+ gems.bundle(groups: groups, gemfile_path: gemfile_path)
115
+ end
116
+
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)
134
+ when :context
135
+ search(@context_directory)
136
+ when :current
137
+ search(::Dir.getwd)
138
+ when :toys
139
+ search_toys
140
+ else
141
+ raise ::ArgumentError, "Unrecognized search_dir: #{dir.inspect}"
142
+ end
143
+ end
144
+
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
166
+ end
110
167
  end
111
168
  end
112
169
  end