toys-core 0.16.0 → 0.17.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c3713d359e1b03fc22968ed9e3254c82c92e157c92146c6d5728d514eb88dc1c
4
- data.tar.gz: d96baedc494f78a534a9f0a00ca64409bdbd25e1cf33ec48d2a16f59824c72b9
3
+ metadata.gz: be1392e6cb8f49c19e9472449943cf873209b939d11cdc1e788bb58a1083c013
4
+ data.tar.gz: d78818495765389e99111032e863dc21d4fa982dfde07780a5b1ff9cc96743df
5
5
  SHA512:
6
- metadata.gz: 4ca769a0b88654941c7369f9fdcf22c532ae53358c554e38817beea217d9d544d20324cd2288c8d961cd37ab2527a22e82289a3726e6d5113a7c6d42f6502a56
7
- data.tar.gz: c50ff7f52713c42eed61ea3e2e81e7b53d1a75ccc89b8ceaff6f4114d71d647018ecf80c529d4254234501108dd5e9f95406e297a64d5064d3f7fa88e8297b47
6
+ metadata.gz: 63ca5e62ed30e068d7b75a0b08e3916fc19466d92ca165d52f8f333e9538f96358027fdd676690042c5eb55d2e2569c3c1a8b8d9c32dd33f7ec3d4036230bc81
7
+ data.tar.gz: 61821ef2ffde83d82469c6a8d38ba092f1efc2a3254a1f848d877fda09a888fc932274e27c742c53d0480a55abc1b49fc6266359ceeca31e8851e16dac3ce0e8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Release History
2
2
 
3
+ ### v0.17.0 / 2025-11-07
4
+
5
+ Toys-Core 0.17 supports several significant new pieces of functionality:
6
+
7
+ * Support for loading tools from Rubygems. The load_gem directive loads tools from the "toys" directory in a gem, installing the gem if necessary. This makes it easy to distribute tools, securely and versioned, as gems.
8
+ * Flag handlers can now take an optional third argument, the entire options hash. This enables significantly more powerful behavior during flag parsing, such as letting flags affect the behavior of other flags.
9
+
10
+ Additional new features:
11
+
12
+ * When using the :gems mixin, you can now specify installation options such as on_missing not only when you include the mixin, but also when you declare the gem.
13
+ * Added support for an environment variable `TOYS_GIT_CACHE_WRITABLE` to disable the read-only behavior of git cache sources. This improves compatibility with environments that want to delete caches.
14
+
15
+ Other fixes and documentation:
16
+
17
+ * Added the standard logger gem to the toys-core dependencies to silence Ruby 3.5 warnings.
18
+ * Updated the user guide to cover new features and fix some internal links
19
+
3
20
  ### v0.16.0 / 2025-10-31
4
21
 
5
22
  * ADDED: Updated minimum Ruby version to 2.7
data/docs/guide.md CHANGED
@@ -159,7 +159,7 @@ If only `foo` is requested, the loader will execute the `tool "foo" do` block
159
159
  to get that tool definition, but will not execute the `tool "bar" do` block.
160
160
 
161
161
  We will discuss more about the features of the loader below in the section on
162
- [defining functionality](#Defining_functionality).
162
+ [defining functionality](#defining-functionality).
163
163
 
164
164
  #### Building context
165
165
 
@@ -209,16 +209,16 @@ Generally, you control CLI features by passing arguments to its constructor.
209
209
  These features include:
210
210
 
211
211
  * How to find toys files and related code and data. See the section on
212
- [defining functionality](#Defining_functionality).
212
+ [defining functionality](#defining-functionality).
213
213
  * Middleware, providing common behavior for all tools. See the section on
214
- [customizing the middleware stack](#Customizing_default_behavior).
214
+ [customizing the middleware stack](#customizing-default-behavior).
215
215
  * Common mixins and templates available to all tools. See the section on
216
- [how to define mixins and templates](#Defining_mixins_and_templates).
216
+ [how to define mixins and templates](#defining-mixins-and-templates).
217
217
  * How logs, errors, and signals are reported. See the section on
218
- [customizing tool output](#Customizing_tool_output).
218
+ [customizing tool output](#customizing-tool-output).
219
219
  * How the executable interacts with the shell, including setting up tab
220
220
  completion. See the
221
- [corresponding section](#Shell_and_command_line_integration).
221
+ [corresponding section](#shell-and-command-line-integration).
222
222
 
223
223
  Each of the actual parameters is covered in detail in the documentation for
224
224
  {Toys::CLI#initialize}. The configuration of a CLI cannot be changed once the
@@ -663,7 +663,7 @@ execute.
663
663
  # This is a context key that will be used to store the "--show-timing"
664
664
  # flag state. We can use `Object.new` to ensure that the key is unique
665
665
  # across other middlewares and tool definitions.
666
- KEY = Object.new
666
+ KEY = Object.new.freeze
667
667
 
668
668
  # This method intercepts tool configuration. We use it to add a flag that
669
669
  # enables timing display.
@@ -416,7 +416,7 @@ module Toys
416
416
  private
417
417
 
418
418
  REMAINING_HANDLER = ->(val, prev) { prev.is_a?(::Array) ? prev << val : [val] }
419
- ARG_HANDLER = ->(val, _prev) { val }
419
+ ARG_HANDLER = ->(val) { val }
420
420
  private_constant :REMAINING_HANDLER, :ARG_HANDLER
421
421
 
422
422
  def initial_data(cli, tool, default_data)
@@ -551,7 +551,12 @@ module Toys
551
551
  value = accept.convert(*Array(match))
552
552
  end
553
553
  if handler
554
- value = handler.call(value, @data[key])
554
+ args = [value, @data[key], @data]
555
+ if handler.lambda?
556
+ limit = handler.arity.negative? ? -handler.arity - 1 : handler.arity
557
+ args = args[...limit]
558
+ end
559
+ value = handler.call(*args)
555
560
  end
556
561
  @data[key] = value
557
562
  end
data/lib/toys/core.rb CHANGED
@@ -9,7 +9,7 @@ module Toys
9
9
  # Current version of Toys core.
10
10
  # @return [String]
11
11
  #
12
- VERSION = "0.16.0"
12
+ VERSION = "0.17.0"
13
13
  end
14
14
 
15
15
  ##
data/lib/toys/dsl/flag.rb CHANGED
@@ -114,17 +114,20 @@ module Toys
114
114
  end
115
115
 
116
116
  ##
117
- # Set the optional handler for setting/updating the value when a flag is
118
- # parsed. A handler should be a Proc taking two arguments, the new given
119
- # value and the previous value, and it should return the new value that
120
- # should be set. You may pass the handler as a Proc (or an object
121
- # responding to the `call` method) or you may pass a block.
122
- #
123
- # You can also pass one of the special values `:set` or `:push` as the
124
- # handler. The `:set` handler replaces the previous value (equivalent to
125
- # `-> (val, _prev) { val }`.) The `:push` handler expects the previous
126
- # value to be an array and pushes the given value onto it; it should be
127
- # combined with setting the default value to `[]` and is intended for
117
+ # Set the optional handler that customizes how a value is set or updated
118
+ # when the flag is parsed.
119
+ #
120
+ # A handler is a proc that takes up to three arguments: the given value,
121
+ # the previous value, and a hash containing all the data collected so far
122
+ # during argument parsing. It must return the new value for the flag. You
123
+ # You may pass the handler as a Proc (or an object responding to the
124
+ # `call` method) or you may provide a block.
125
+ #
126
+ # You may also specify a predefined named handler. The `:set` handler
127
+ # (the default) replaces the previous value (effectively
128
+ # `-> (val) { val }`). The `:push` handler expects the previous value to
129
+ # be an array and pushes the given value onto it; it should be combined
130
+ # with setting the default value to `[]` and is intended for
128
131
  # "multi-valued" flags.
129
132
  #
130
133
  # @param handler [Proc,:set,:push]
@@ -147,15 +147,18 @@ module Toys
147
147
  # @param default [Object] The default value. This is the value that will
148
148
  # be set in the context if this flag is not provided on the command
149
149
  # line. Defaults to `nil`.
150
- # @param handler [Proc,nil,:set,:push] An optional handler for
151
- # setting/updating the value. A handler is a proc taking two
152
- # arguments, the given value and the previous value, returning the
153
- # new value that should be set. You may also specify a predefined
154
- # named handler. The `:set` handler (the default) replaces the
155
- # previous value (effectively `-> (val, _prev) { val }`). The
156
- # `:push` handler expects the previous value to be an array and
157
- # pushes the given value onto it; it should be combined with setting
158
- # `default: []` and is intended for "multi-valued" flags.
150
+ # @param handler [Proc,nil,:set,:push] An optional handler that
151
+ # customizes how a value is set or updated when the flag is parsed.
152
+ # A handler is a proc that takes up to three arguments: the given
153
+ # value, the previous value, and a hash containing all the data
154
+ # collected so far during argument parsing. The proc must return the
155
+ # new value for the flag.
156
+ # You may also specify a predefined named handler. The `:set` handler
157
+ # (the default) replaces the previous value (effectively
158
+ # `-> (val) { val }`). The `:push` handler expects the previous value
159
+ # to be an array and pushes the given value onto it; it should be
160
+ # combined with setting the default value to `[]` and is intended for
161
+ # "multi-valued" flags.
159
162
  # @param complete_flags [Object] A specifier for shell tab completion
160
163
  # for flag names associated with this flag. By default, a
161
164
  # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
data/lib/toys/dsl/tool.rb CHANGED
@@ -436,9 +436,12 @@ module Toys
436
436
  # current commit if already loading from git, or to `HEAD`.
437
437
  # @param as [String] Load into the given tool/namespace. If omitted,
438
438
  # configuration will be loaded into the current namespace.
439
- # @param update [Boolean] Force-fetch from the remote (unless the commit
440
- # is a SHA). This will ensure that symbolic commits, such as branch
441
- # names, are up to date. Default is false.
439
+ # @param update [Boolean,Integer] Whether and when to force-fetch from
440
+ # the remote (unless the commit is a SHA). Force-fetching will ensure
441
+ # that symbolic commits, such as branch names or HEAD, are up to date.
442
+ # You can pass `true` or `false` to specify whether to update, or an
443
+ # integer to update if the last update was done at least that many
444
+ # seconds ago. Default is false.
442
445
  #
443
446
  # @return [self]
444
447
  #
@@ -453,9 +456,35 @@ module Toys
453
456
  raise ToolDefinitionError, "Git remote not specified" unless remote
454
457
  path ||= ""
455
458
  commit ||= source_info.git_commit || "HEAD"
456
- @__loader.load_git(source_info, remote, path, commit,
457
- @__words, @__remaining_words, @__priority,
458
- update: update)
459
+ @__loader.load_git(source_info, remote, path, commit, update,
460
+ @__words, @__remaining_words, @__priority)
461
+ self
462
+ end
463
+
464
+ ##
465
+ # Load configuration from a gem, as if its contents were inserted at the
466
+ # current location.
467
+ #
468
+ # @param name [String] Name of the gem
469
+ # @param version [String,Array<String>] Version requirements for the gem.
470
+ # @param path [String] Optional path within the gem to the file or
471
+ # directory to load. Defaults to the root of the gem's toys directory.
472
+ # @param toys_dir [String] Optional override for the gem's toys
473
+ # directory name. If not specified, the default specified by the gem
474
+ # will be used.
475
+ # @param as [String] Load into the given tool/namespace. If omitted,
476
+ # configuration will be loaded into the current namespace.
477
+ #
478
+ def load_gem(name, version: nil, path: nil, toys_dir: nil, as: nil)
479
+ if as
480
+ tool(as) do
481
+ load_gem(name, version: version, path: path, toys_dir: toys_dir)
482
+ end
483
+ return self
484
+ end
485
+ path ||= ""
486
+ @__loader.load_gem(source_info, name, version, toys_dir, path,
487
+ @__words, @__remaining_words, @__priority)
459
488
  self
460
489
  end
461
490
 
@@ -946,15 +975,18 @@ module Toys
946
975
  # @param default [Object] The default value. This is the value that will
947
976
  # be set in the context if this flag is not provided on the command
948
977
  # line. Defaults to `nil`.
949
- # @param handler [Proc,nil,:set,:push] An optional handler for
950
- # setting/updating the value. A handler is a proc taking two
951
- # arguments, the given value and the previous value, returning the
952
- # new value that should be set. You may also specify a predefined
953
- # named handler. The `:set` handler (the default) replaces the
954
- # previous value (effectively `-> (val, _prev) { val }`). The
955
- # `:push` handler expects the previous value to be an array and
956
- # pushes the given value onto it; it should be combined with setting
957
- # `default: []` and is intended for "multi-valued" flags.
978
+ # @param handler [Proc,nil,:set,:push] An optional handler that
979
+ # customizes how a value is set or updated when the flag is parsed.
980
+ # A handler is a proc that takes up to three arguments: the given
981
+ # value, the previous value, and a hash containing all the data
982
+ # collected so far during argument parsing. The proc must return the
983
+ # new value for the flag.
984
+ # You may also specify a predefined named handler. The `:set` handler
985
+ # (the default) replaces the previous value (effectively
986
+ # `-> (val) { val }`). The `:push` handler expects the previous value
987
+ # to be an array and pushes the given value onto it; it should be
988
+ # combined with setting the default value to `[]` and is intended for
989
+ # "multi-valued" flags.
958
990
  # @param complete_flags [Object] A specifier for shell tab completion
959
991
  # for flag names associated with this flag. By default, a
960
992
  # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
data/lib/toys/flag.rb CHANGED
@@ -374,16 +374,16 @@ module Toys
374
374
  # The set handler replaces the previous value.
375
375
  # @return [Proc]
376
376
  #
377
- SET_HANDLER = ->(val, _prev) { val }
377
+ SET_HANDLER = proc { |val| val }
378
378
 
379
379
  ##
380
380
  # The push handler pushes the given value using the `<<` operator.
381
381
  # @return [Proc]
382
382
  #
383
- PUSH_HANDLER = ->(val, prev) { prev.nil? ? [val] : prev << val }
383
+ PUSH_HANDLER = proc { |val, prev| prev.nil? ? [val] : prev << val }
384
384
 
385
385
  ##
386
- # The default handler is the set handler, replacing the previous value.
386
+ # The default handler is the set handler, which replaces the previous value.
387
387
  # @return [Proc]
388
388
  #
389
389
  DEFAULT_HANDLER = SET_HANDLER
@@ -401,15 +401,18 @@ module Toys
401
401
  # @param default [Object] The default value. This is the value that will
402
402
  # be set in the context if this flag is not provided on the command
403
403
  # line. Defaults to `nil`.
404
- # @param handler [Proc,nil,:set,:push] An optional handler for
405
- # setting/updating the value. A handler is a proc taking two
406
- # arguments, the given value and the previous value, returning the
407
- # new value that should be set. You may also specify a predefined
408
- # named handler. The `:set` handler (the default) replaces the
409
- # previous value (effectively `-> (val, _prev) { val }`). The
410
- # `:push` handler expects the previous value to be an array and
411
- # pushes the given value onto it; it should be combined with setting
412
- # `default: []` and is intended for "multi-valued" flags.
404
+ # @param handler [Proc,nil,:set,:push] An optional handler that customizes
405
+ # how a value is set or updated when the flag is parsed.
406
+ # A handler is a proc that takes up to three arguments: the given
407
+ # value, the previous value, and a hash containing all the data
408
+ # collected so far during argument parsing. The proc must return the
409
+ # new value for the flag.
410
+ # You may also specify a predefined named handler. The `:set` handler
411
+ # (the default) replaces the previous value (effectively
412
+ # `-> (val) { val }`). The `:push` handler expects the previous value
413
+ # to be an array and pushes the given value onto it; it should be
414
+ # combined with setting `default: []` and is intended for
415
+ # "multi-valued" flags.
413
416
  # @param complete_flags [Object] A specifier for shell tab completion for
414
417
  # flag names associated with this flag. By default, a
415
418
  # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
data/lib/toys/loader.rb CHANGED
@@ -49,7 +49,8 @@ module Toys
49
49
  mixin_lookup: nil,
50
50
  middleware_lookup: nil,
51
51
  template_lookup: nil,
52
- git_cache: nil)
52
+ git_cache: nil,
53
+ gems_util: nil)
53
54
  if index_file_name && ::File.extname(index_file_name) != ".rb"
54
55
  raise ::ArgumentError, "Illegal index file name #{index_file_name.inspect}"
55
56
  end
@@ -73,6 +74,7 @@ module Toys
73
74
  @middleware_stack = Middleware.stack(middleware_stack)
74
75
  @delimiter_handler = DelimiterHandler.new(extra_delimiters)
75
76
  @git_cache = git_cache
77
+ @gems_util = gems_util
76
78
  get_tool([], -999_999)
77
79
  end
78
80
 
@@ -204,8 +206,7 @@ module Toys
204
206
  high_priority: false,
205
207
  update: false,
206
208
  context_directory: nil)
207
- git_cache = @git_cache || Loader.default_git_cache
208
- path = git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
209
+ path = resolve_git_path(git_remote, git_path, git_commit, update)
209
210
  @mutex.synchronize do
210
211
  raise "Cannot add a git source after tool loading has started" if @loading_started
211
212
  priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
@@ -219,6 +220,43 @@ module Toys
219
220
  self
220
221
  end
221
222
 
223
+ ##
224
+ # Add a configuration gem source to the loader.
225
+ #
226
+ # @param gem_name [String] The name of the gem
227
+ # @param gem_version [String,Array<String>] The version requirements
228
+ # @param gem_path [String] The path from the gem's toys directory to the
229
+ # relevant file or directory. Specify the empty string to use the
230
+ # entire toys directory.
231
+ # @param high_priority [Boolean] If true, add this path at the top of the
232
+ # priority list. Defaults to false, indicating the new path should be
233
+ # at the bottom of the priority list.
234
+ # @param gem_toys_dir [String] The name of the toys directory. Optional.
235
+ # Defaults to the directory specified in the gem's metadata, or the
236
+ # value "toys".
237
+ # @param context_directory [String,nil] The context directory for tools
238
+ # loaded from this source. You can pass a directory path as a string,
239
+ # or `nil` to denote no context. Defaults to `nil`.
240
+ # @return [self]
241
+ #
242
+ def add_gem(gem_name, gem_version, gem_path,
243
+ high_priority: false,
244
+ gem_toys_dir: nil,
245
+ context_directory: nil)
246
+ gem_version, gem_path, path = resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
247
+ @mutex.synchronize do
248
+ raise "Cannot add a gem source after tool loading has started" if @loading_started
249
+ priority = high_priority ? (@max_priority += 1) : (@min_priority -= 1)
250
+ source = SourceInfo.create_gem_root(gem_name, gem_version, gem_path, path, priority,
251
+ context_directory: context_directory,
252
+ data_dir_name: @data_dir_name,
253
+ lib_dir_name: @lib_dir_name)
254
+ @roots_by_priority[priority] = source
255
+ @worklist << [source, [], priority]
256
+ end
257
+ self
258
+ end
259
+
222
260
  ##
223
261
  # Given a list of command line arguments, find the appropriate tool to
224
262
  # handle the command, loading it from the configuration if necessary.
@@ -439,8 +477,7 @@ module Toys
439
477
  #
440
478
  def load_path(parent_source, path, words, remaining_words, priority)
441
479
  if parent_source.git_remote
442
- raise LoaderError,
443
- "Git source #{parent_source.source_name} tried to load from the local file system"
480
+ raise LoaderError, "Git source #{parent_source.source_name} tried to load from the local file system"
444
481
  end
445
482
  source = parent_source.absolute_child(path)
446
483
  @mutex.synchronize do
@@ -454,16 +491,30 @@ module Toys
454
491
  #
455
492
  # @private This interface is internal and subject to change without warning.
456
493
  #
457
- def load_git(parent_source, git_remote, git_path, git_commit, words, remaining_words, priority,
458
- update: false)
459
- git_cache = @git_cache || Loader.default_git_cache
460
- path = git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
494
+ def load_git(parent_source, git_remote, git_path, git_commit, update,
495
+ words, remaining_words, priority)
496
+ path = resolve_git_path(git_remote, git_path, git_commit, update)
461
497
  source = parent_source.git_child(git_remote, git_path, git_commit, path)
462
498
  @mutex.synchronize do
463
499
  load_validated_path(source, words, remaining_words, priority)
464
500
  end
465
501
  end
466
502
 
503
+ ##
504
+ # Load configuration from the given gem. This is called from the `load_gem`
505
+ # directive in the DSL.
506
+ #
507
+ # @private This interface is internal and subject to change without warning.
508
+ #
509
+ def load_gem(parent_source, gem_name, gem_version, gem_toys_dir, gem_path,
510
+ words, remaining_words, priority)
511
+ gem_version, gem_path, path = resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
512
+ source = parent_source.gem_child(gem_name, gem_version, gem_path, path)
513
+ @mutex.synchronize do
514
+ load_validated_path(source, words, remaining_words, priority)
515
+ end
516
+ end
517
+
467
518
  ##
468
519
  # Load a subtool block. Called from the `tool` directive in the DSL.
469
520
  #
@@ -478,6 +529,7 @@ module Toys
478
529
 
479
530
  @git_cache_mutex = ::Mutex.new
480
531
  @default_git_cache = nil
532
+ @default_gems_util = nil
481
533
 
482
534
  ##
483
535
  # Get a global default GitCache.
@@ -493,6 +545,20 @@ module Toys
493
545
  end
494
546
  end
495
547
 
548
+ ##
549
+ # Get a global default Gems utility.
550
+ #
551
+ # @private This interface is internal and subject to change without warning.
552
+ #
553
+ def self.default_gems_util
554
+ @git_cache_mutex.synchronize do
555
+ @default_gems_util ||= begin
556
+ require "toys/utils/gems"
557
+ Utils::Gems.new
558
+ end
559
+ end
560
+ end
561
+
496
562
  ##
497
563
  # Determine the next setting for remaining_words, given a word.
498
564
  #
@@ -640,6 +706,28 @@ module Toys
640
706
 
641
707
  private
642
708
 
709
+ ##
710
+ # Resolve the file system path to the given object in the git cache
711
+ #
712
+ def resolve_git_path(git_remote, git_path, git_commit, update)
713
+ git_cache = @git_cache || Loader.default_git_cache
714
+ git_cache.get(git_remote, path: git_path, commit: git_commit, update: update)
715
+ end
716
+
717
+ ##
718
+ # Resolve information for a gem source.
719
+ #
720
+ def resolve_gem_info(gem_name, gem_version, gem_toys_dir, gem_path)
721
+ gems_util = @gems_util || Loader.default_gems_util
722
+ gems_util.activate(gem_name, *Array(gem_version))
723
+ gem_spec = ::Gem.loaded_specs[gem_name]
724
+ raise LoaderError, "Unable to find gem #{gem_name}" unless gem_spec&.gem_dir
725
+ gem_toys_dir ||= gem_spec.metadata["toys_dir"] || "toys"
726
+ gem_path = gem_path ? ::File.join(gem_toys_dir, gem_path) : gem_toys_dir
727
+ path = ::File.join(gem_spec.gem_dir, gem_path)
728
+ [gem_spec.version, gem_path, path]
729
+ end
730
+
643
731
  ##
644
732
  # Return a snapshot of all the current tool definitions that have been
645
733
  # loaded. No additional loading is done. The returned array is not in any
@@ -11,6 +11,7 @@ module Toys
11
11
  # * A toys directory
12
12
  # * A single toys file
13
13
  # * A file or directory loaded from git
14
+ # * A file or directory loaded from a gem
14
15
  # * A config block passed directly to the CLI
15
16
  # * A tool block within a toys file
16
17
  #
@@ -143,6 +144,32 @@ module Toys
143
144
  #
144
145
  attr_reader :git_commit
145
146
 
147
+ ##
148
+ # The gem name. This is set if the source, or one of its ancestors, comes
149
+ # from a gem.
150
+ #
151
+ # @return [String] The gem name.
152
+ # @return [nil] if this source is not from a gem.
153
+ #
154
+ attr_reader :gem_name
155
+
156
+ ##
157
+ # The gem version. This is set if the source, or one of its ancestors,
158
+ # comes from a gem.
159
+ #
160
+ # @return [Gem::Version] The gem version.
161
+ # @return [nil] if this source is not from a gem.
162
+ #
163
+ attr_reader :gem_version
164
+
165
+ ##
166
+ # The path within the gem, including the toys root directory in the gem.
167
+ #
168
+ # @return [String] The path.
169
+ # @return [nil] if this source is not from a gem.
170
+ #
171
+ attr_reader :gem_path
172
+
146
173
  ##
147
174
  # A user-visible name of this source.
148
175
  #
@@ -191,8 +218,10 @@ module Toys
191
218
  #
192
219
  # @private This interface is internal and subject to change without warning.
193
220
  #
194
- def initialize(parent, priority, context_directory, source_type, source_path, source_proc,
195
- git_remote, git_path, git_commit, source_name, data_dir_name, lib_dir_name)
221
+ def initialize(parent, priority, context_directory,
222
+ source_type, source_path, source_proc,
223
+ git_remote, git_path, git_commit, gem_name, gem_version, gem_path,
224
+ source_name, data_dir_name, lib_dir_name)
196
225
  @parent = parent
197
226
  @root = parent&.root || self
198
227
  @priority = priority
@@ -204,7 +233,10 @@ module Toys
204
233
  @git_remote = git_remote
205
234
  @git_path = git_path
206
235
  @git_commit = git_commit
207
- @source_name = source_name
236
+ @gem_name = gem_name
237
+ @gem_version = gem_version
238
+ @gem_path = gem_path
239
+ @source_name = source_name || default_source_name
208
240
  @data_dir_name = data_dir_name
209
241
  @lib_dir_name = lib_dir_name
210
242
  @data_dir = find_special_dir(data_dir_name)
@@ -220,18 +252,12 @@ module Toys
220
252
  unless source_type == :directory
221
253
  raise LoaderError, "relative_child is valid only on a directory source"
222
254
  end
223
- child_path = ::File.join(source_path, filename)
224
- child_path, type = SourceInfo.check_path(child_path, true)
255
+ child_path, type = SourceInfo.check_path(::File.join(source_path, filename), true)
225
256
  return nil unless child_path
226
- child_git_path = ::File.join(git_path, filename) if git_path
227
- source_name ||=
228
- if git_path
229
- "git(remote=#{git_remote} path=#{child_git_path} commit=#{git_commit})"
230
- else
231
- child_path
232
- end
257
+ child_git_path = git_path.empty? ? filename : ::File.join(git_path, filename) if git_path
258
+ child_gem_path = gem_path.empty? ? filename : ::File.join(gem_path, filename) if gem_path
233
259
  SourceInfo.new(self, priority, context_directory, type, child_path, nil,
234
- git_remote, child_git_path, git_commit,
260
+ git_remote, child_git_path, git_commit, gem_name, gem_version, child_gem_path,
235
261
  source_name, @data_dir_name, @lib_dir_name)
236
262
  end
237
263
 
@@ -242,8 +268,8 @@ module Toys
242
268
  #
243
269
  def absolute_child(child_path, source_name: nil)
244
270
  child_path, type = SourceInfo.check_path(child_path, false)
245
- source_name ||= child_path
246
- SourceInfo.new(self, priority, context_directory, type, child_path, nil, nil, nil, nil,
271
+ SourceInfo.new(self, priority, context_directory, type, child_path, nil,
272
+ nil, nil, nil, nil, nil, nil,
247
273
  source_name, @data_dir_name, @lib_dir_name)
248
274
  end
249
275
 
@@ -252,13 +278,22 @@ module Toys
252
278
  #
253
279
  # @private This interface is internal and subject to change without warning.
254
280
  #
255
- def git_child(child_git_remote, child_git_path, child_git_commit, child_path,
256
- source_name: nil)
281
+ def git_child(child_git_remote, child_git_path, child_git_commit, child_path, source_name: nil)
282
+ child_path, type = SourceInfo.check_path(child_path, false)
283
+ SourceInfo.new(self, priority, context_directory, type, child_path, nil,
284
+ child_git_remote, child_git_path, child_git_commit, nil, nil, nil,
285
+ source_name, @data_dir_name, @lib_dir_name)
286
+ end
287
+
288
+ ##
289
+ # Create a child SourceInfo with a gem source.
290
+ #
291
+ # @private This interface is internal and subject to change without warning.
292
+ #
293
+ def gem_child(child_gem_name, child_gem_version, child_gem_path, child_path, source_name: nil)
257
294
  child_path, type = SourceInfo.check_path(child_path, false)
258
- source_name ||=
259
- "git(remote=#{child_git_remote} path=#{child_git_path} commit=#{child_git_commit})"
260
295
  SourceInfo.new(self, priority, context_directory, type, child_path, nil,
261
- child_git_remote, child_git_path, child_git_commit,
296
+ nil, nil, nil, child_gem_name, child_gem_version, child_gem_path,
262
297
  source_name, @data_dir_name, @lib_dir_name)
263
298
  end
264
299
 
@@ -270,7 +305,7 @@ module Toys
270
305
  def proc_child(child_proc, source_name: nil)
271
306
  source_name ||= self.source_name
272
307
  SourceInfo.new(self, priority, context_directory, :proc, source_path, child_proc,
273
- git_remote, git_path, git_commit,
308
+ git_remote, git_path, git_commit, gem_name, gem_version, gem_path,
274
309
  source_name, @data_dir_name, @lib_dir_name)
275
310
  end
276
311
 
@@ -291,8 +326,8 @@ module Toys
291
326
  when :path
292
327
  context_directory = source_path
293
328
  end
294
- source_name ||= source_path
295
- new(nil, priority, context_directory, type, source_path, nil, nil, nil, nil,
329
+ new(nil, priority, context_directory, type, source_path, nil,
330
+ nil, nil, nil, nil, nil, nil,
296
331
  source_name, data_dir_name, lib_dir_name)
297
332
  end
298
333
 
@@ -307,9 +342,25 @@ module Toys
307
342
  lib_dir_name: nil,
308
343
  source_name: nil)
309
344
  source_path, type = check_path(source_path, false)
310
- source_name ||= "git(remote=#{git_remote} path=#{git_path} commit=#{git_commit})"
311
- new(nil, priority, context_directory, type, source_path, nil, git_remote,
312
- git_path, git_commit, source_name, data_dir_name, lib_dir_name)
345
+ new(nil, priority, context_directory, type, source_path, nil,
346
+ git_remote, git_path, git_commit, nil, nil, nil,
347
+ source_name, data_dir_name, lib_dir_name)
348
+ end
349
+
350
+ ##
351
+ # Create a root source info for a loaded gem.
352
+ #
353
+ # @private This interface is internal and subject to change without warning.
354
+ #
355
+ def self.create_gem_root(gem_name, gem_version, gem_path, source_path, priority,
356
+ context_directory: nil,
357
+ data_dir_name: nil,
358
+ lib_dir_name: nil,
359
+ source_name: nil)
360
+ source_path, type = check_path(source_path, false)
361
+ new(nil, priority, context_directory, type, source_path, nil,
362
+ nil, nil, nil, gem_name, gem_version, gem_path,
363
+ source_name, data_dir_name, lib_dir_name)
313
364
  end
314
365
 
315
366
  ##
@@ -322,9 +373,9 @@ module Toys
322
373
  data_dir_name: nil,
323
374
  lib_dir_name: nil,
324
375
  source_name: nil)
325
- source_name ||= "(code block #{source_proc.object_id})"
326
- new(nil, priority, context_directory, :proc, nil, source_proc, nil, nil,
327
- nil, source_name, data_dir_name, lib_dir_name)
376
+ new(nil, priority, context_directory, :proc, nil, source_proc,
377
+ nil, nil, nil, nil, nil, nil,
378
+ source_name, data_dir_name, lib_dir_name)
328
379
  end
329
380
 
330
381
  ##
@@ -354,6 +405,18 @@ module Toys
354
405
 
355
406
  private
356
407
 
408
+ def default_source_name
409
+ if @git_remote
410
+ "git(remote=#{@git_remote} path=#{@git_path} commit=#{@git_commit})"
411
+ elsif @gem_name
412
+ "gem(name=#{@gem_name} version=#{@gem_version} path=#{@gem_path})"
413
+ elsif @source_type == :proc
414
+ "(code block #{@source_proc.object_id})"
415
+ else
416
+ @source_path
417
+ end
418
+ end
419
+
357
420
  def find_special_dir(dir_name)
358
421
  return nil if @source_type != :directory || dir_name.nil?
359
422
  dir = ::File.join(@source_path, dir_name)
@@ -22,8 +22,8 @@ module Toys
22
22
  #
23
23
  # tool "my_tool" do
24
24
  # include :gems
25
- # gem "nokogiri", "~> 1.15"
26
25
  # def run
26
+ # gem "nokogiri", "~> 1.15"
27
27
  # # Do stuff with Nokogiri
28
28
  # end
29
29
  # end
@@ -33,6 +33,18 @@ module Toys
33
33
  #
34
34
  # include :gems, on_missing: :error
35
35
  #
36
+ # You can also pass options to the {#gem} mixin method itself:
37
+ #
38
+ # tool "my_tool" do
39
+ # include :gems
40
+ # def run
41
+ # # If the gem is not installed, error out instead of asking to
42
+ # # install it.
43
+ # gem "nokogiri", "~> 1.15", on_missing: :error
44
+ # # Do stuff with Nokogiri
45
+ # end
46
+ # end
47
+ #
36
48
  # See {Toys::Utils::Gems#initialize} for a list of supported options.
37
49
  #
38
50
  module Gems
@@ -54,8 +66,8 @@ module Toys
54
66
  # @param requirements [String...] Version requirements
55
67
  # @return [void]
56
68
  #
57
- def gem(name, *requirements)
58
- self.class.gems.activate(name, *requirements)
69
+ def gem(name, *requirements, **options)
70
+ self.class.gem(name, *requirements, **options)
59
71
  end
60
72
 
61
73
  on_include do |**opts|
@@ -76,8 +88,15 @@ module Toys
76
88
  ##
77
89
  # @private
78
90
  #
79
- def self.gem(name, *requirements)
80
- gems.activate(name, *requirements)
91
+ def self.gem(name, *requirements, **options)
92
+ gems_util =
93
+ if options.empty?
94
+ gems
95
+ else
96
+ require "toys/utils/gems"
97
+ Utils::Gems.new(**options)
98
+ end
99
+ gems_util.activate(name, *requirements)
81
100
  end
82
101
  end
83
102
  end
@@ -1012,15 +1012,16 @@ module Toys
1012
1012
  # @param default [Object] The default value. This is the value that will
1013
1013
  # be set in the context if this flag is not provided on the command
1014
1014
  # line. Defaults to `nil`.
1015
- # @param handler [Proc,nil,:set,:push] An optional handler for
1016
- # setting/updating the value. A handler is a proc taking two
1017
- # arguments, the given value and the previous value, returning the
1018
- # new value that should be set. You may also specify a predefined
1019
- # named handler. The `:set` handler (the default) replaces the
1020
- # previous value (effectively `-> (val, _prev) { val }`). The
1021
- # `:push` handler expects the previous value to be an array and
1022
- # pushes the given value onto it; it should be combined with setting
1023
- # `default: []` and is intended for "multi-valued" flags.
1015
+ # @param handler [Proc,nil,:set,:push] An optional handler that customizes
1016
+ # how a value is set or updated. A handler is a proc that takes up to
1017
+ # three arguments: the given value, the previous value, and a hash
1018
+ # containing all the data collected so far during argument parsing. It
1019
+ # must return the new value that should be set. You may also specify a
1020
+ # predefined named handler. The `:set` handler (the default) replaces
1021
+ # the previous value (effectively `-> (val) { val }`). The `:push`
1022
+ # handler expects the previous value to be an array and pushes the
1023
+ # given value onto it; it should be combined with setting `default: []`
1024
+ # and is intended for "multi-valued" flags.
1024
1025
  # @param complete_flags [Object] A specifier for shell tab completion
1025
1026
  # for flag names associated with this flag. By default, a
1026
1027
  # {Toys::Flag::DefaultCompletion} is used, which provides the flag's
@@ -274,6 +274,21 @@ module Toys
274
274
  end
275
275
  end
276
276
 
277
+ ##
278
+ # Returns whether shared source files are writable by default.
279
+ # Normally, shared sources are made read-only to protect them from being
280
+ # modified accidentally since multiple clients may be accessing them.
281
+ # However, you can disable this feature by setting the environment
282
+ # variable `TOYS_GIT_CACHE_WRITABLE` to any non-empty value. This can be
283
+ # useful in environments that want to clean up temporary directories and
284
+ # are being hindered by read-only files.
285
+ #
286
+ # @return [boolean]
287
+ #
288
+ def self.sources_writable?
289
+ !::ENV["TOYS_GIT_CACHE_WRITABLE"].to_s.empty?
290
+ end
291
+
277
292
  ##
278
293
  # Access a git cache.
279
294
  #
@@ -318,7 +333,7 @@ module Toys
318
333
  # @param into [String] If provided, copies the specified files into the
319
334
  # given directory path. If omitted or `nil`, populates and returns a
320
335
  # shared source file or directory.
321
- # @param update [Boolean,Integer] Whether to update of non-SHA commit
336
+ # @param update [Boolean,Integer] Whether to update non-SHA commit
322
337
  # references if they were previously loaded. This is useful, for
323
338
  # example, if the commit is `HEAD` or a branch name. Pass `true` or
324
339
  # `false` to specify whether to update, or an integer to update if
@@ -569,7 +584,7 @@ module Toys
569
584
  ::FileUtils.mkdir_p(source_path)
570
585
  ::FileUtils.chmod_R("u+w", source_path, force: true)
571
586
  copy_from_repo(repo_path, source_path, sha, path)
572
- ::FileUtils.chmod_R("a-w", source_path, force: true)
587
+ ::FileUtils.chmod_R("a-w", source_path, force: true) unless GitCache.sources_writable?
573
588
  end
574
589
  repo_lock.access_source!(sha, path)
575
590
  path == "." ? source_path : ::File.join(source_path, path)
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toys-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Azuma
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: logger
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.4'
12
26
  description: Toys-Core is the command line tool framework underlying Toys. It can
13
27
  be used to create command line executables using the Toys DSL and classes.
14
28
  email:
@@ -78,10 +92,10 @@ homepage: https://github.com/dazuma/toys
78
92
  licenses:
79
93
  - MIT
80
94
  metadata:
81
- changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.16.0/file.CHANGELOG.html
95
+ changelog_uri: https://dazuma.github.io/toys/gems/toys-core/v0.17.0/file.CHANGELOG.html
82
96
  source_code_uri: https://github.com/dazuma/toys/tree/main/toys-core
83
97
  bug_tracker_uri: https://github.com/dazuma/toys/issues
84
- documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.16.0
98
+ documentation_uri: https://dazuma.github.io/toys/gems/toys-core/v0.17.0
85
99
  rdoc_options: []
86
100
  require_paths:
87
101
  - lib