rake-commander 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +12 -8
  4. data/CHANGELOG.md +69 -4
  5. data/LICENSE +21 -0
  6. data/README.md +94 -2
  7. data/Rakefile +11 -13
  8. data/examples/01_basic_example.rb +28 -0
  9. data/examples/02_a_chainer_example.rb +66 -0
  10. data/examples/02_a_chainer_options_set.rb +8 -0
  11. data/examples/02_b_chained_example.rb +13 -0
  12. data/examples/03_a_chainer_plus_example.rb +34 -0
  13. data/examples/03_b_chained_plus_example.rb +17 -0
  14. data/examples/Examples.rake +7 -0
  15. data/examples/README.md +79 -0
  16. data/examples/libs/shell_helpers.rb +81 -0
  17. data/lib/rake-commander/base/class_auto_loader.rb +45 -7
  18. data/lib/rake-commander/base/class_helpers.rb +16 -61
  19. data/lib/rake-commander/base/class_inheritable.rb +122 -0
  20. data/lib/rake-commander/base/custom_error.rb +52 -0
  21. data/lib/rake-commander/base/object_helpers.rb +42 -0
  22. data/lib/rake-commander/base.rb +16 -2
  23. data/lib/rake-commander/option.rb +115 -25
  24. data/lib/rake-commander/options/arguments.rb +206 -94
  25. data/lib/rake-commander/options/description.rb +17 -0
  26. data/lib/rake-commander/options/error/base.rb +86 -0
  27. data/lib/rake-commander/options/error/handling.rb +106 -0
  28. data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
  29. data/lib/rake-commander/options/error/invalid_option.rb +9 -0
  30. data/lib/rake-commander/options/error/missing_argument.rb +10 -0
  31. data/lib/rake-commander/options/error/missing_option.rb +48 -0
  32. data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
  33. data/lib/rake-commander/options/error.rb +75 -10
  34. data/lib/rake-commander/options/name.rb +67 -23
  35. data/lib/rake-commander/options/result.rb +107 -0
  36. data/lib/rake-commander/options/set.rb +7 -1
  37. data/lib/rake-commander/options.rb +175 -98
  38. data/lib/rake-commander/patcher/README.md +79 -0
  39. data/lib/rake-commander/patcher/application/run_method.rb +46 -0
  40. data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
  41. data/lib/rake-commander/patcher/application.rb +16 -0
  42. data/lib/rake-commander/patcher/base.rb +45 -0
  43. data/lib/rake-commander/patcher/debug.rb +32 -0
  44. data/lib/rake-commander/patcher/helpers.rb +44 -0
  45. data/lib/rake-commander/patcher.rb +26 -0
  46. data/lib/rake-commander/rake_context/wrapper.rb +2 -0
  47. data/lib/rake-commander/rake_task.rb +49 -54
  48. data/lib/rake-commander/version.rb +1 -1
  49. data/lib/rake-commander.rb +4 -0
  50. data/rake-commander.gemspec +4 -1
  51. metadata +74 -6
  52. data/examples/basic.rb +0 -30
  53. data/lib/rake-commander/options/error_rely.rb +0 -58
@@ -1,5 +1,8 @@
1
1
  require_relative 'options/name'
2
+ require_relative 'options/description'
2
3
  require_relative 'options/arguments'
4
+ require_relative 'options/result'
5
+ require_relative 'options/error'
3
6
  require_relative 'option'
4
7
 
5
8
  class RakeCommander
@@ -8,160 +11,234 @@ class RakeCommander
8
11
  def included(base)
9
12
  super(base)
10
13
  base.extend RakeCommander::Base::ClassHelpers
14
+ base.extend RakeCommander::Base::ClassInheritable
11
15
  base.extend ClassMethods
12
- base.inheritable_attrs :banner, :options_hash, :results_with_all_defaults
13
- base.extend RakeCommander::Options::Arguments
16
+ base.attr_inheritable :banner, :options_hash
17
+ base.class_resolver :option_class, RakeCommander::Option
18
+ base.send :include, RakeCommander::Options::Result
19
+ base.send :include, RakeCommander::Options::Error
20
+ base.send :include, RakeCommander::Options::Arguments
14
21
  end
15
22
  end
16
23
 
17
24
  module ClassMethods
18
- def options_hash
19
- @options_hash ||= {}
20
- end
21
-
22
- def options
23
- options_hash.values.uniq
25
+ # Overrides the auto-generated banner
26
+ def banner(desc = :not_used)
27
+ return @banner = desc unless desc == :not_used
28
+ return @banner if @banner
29
+ return task_options_banner if respond_to?(:task_options_banner, true)
24
30
  end
25
31
 
26
- def options?
27
- !options.empty?
32
+ # Defines a new option or opens for edition an existing one if `reopen: true` is used.
33
+ # @note
34
+ # - If override is `true`, it will with a Warning when same `short` or `name` clashes.
35
+ def option(*args, override: true, reopen: false, **kargs, &block)
36
+ return option_reopen(*args, override: override, **kargs, &block) if reopen
37
+ opt = option_class.new(*args, **kargs, &block)
38
+ add_to_options(opt, override: override)
28
39
  end
29
40
 
30
- def clear_options!
31
- @options_hash = {}
41
+ # It re-opens an option for edition. If it does not exist, it **upserts** it.
42
+ # @note
43
+ # 1. If `override` is `false, it will fail to change the `name` or the `short`
44
+ # when they are already taken by some other option.
45
+ # 2. It will have the effect of overriding existing options
46
+ # @note when `short` and `name` are provided, `name` takes precedence over `short`
47
+ # in the lookup (to identify the existing option)
48
+ def option_reopen(*args, override: false, **kargs, &block)
49
+ aux = option_class.new(*args, **kargs, sample: true, &block)
50
+ opt = options_hash.values_at(aux.name, aux.short).compact.first
51
+ return option(*args, **kargs, &block) unless opt
52
+ replace_in_options(opt, opt.merge(aux), override: override)
53
+ end
54
+
55
+ # Removes options with short or name `keys` from options
56
+ def option_remove(*keys)
57
+ keys.map do |key|
58
+ aux = option_class.new(key, sample: true)
59
+ opt = options_hash.values_at(aux.name, aux.short).compact.first
60
+ delete_from_options(opt) if opt
61
+ end
32
62
  end
33
63
 
34
64
  # Allows to use a set of options
65
+ # @note it does a deep dup of each option.
66
+ # @param override [Boolean] wheter existing options with same option name
67
+ # should be overriden, may they clash
35
68
  # @param options [Enumerable<RakeCommander::Option>]
36
- def use_options(options)
37
- options = options.values if options.is_a?(Hash)
38
- options.each do |opt|
39
- add_to_options(opt)
69
+ def options_use(opts, override: true)
70
+ raise "Could not obtain list of RakeCommander::Option from #{opts.class}" unless opts = to_options(opts)
71
+ opts.each do |opt|
72
+ add_to_options(opt.deep_dup, override: override)
40
73
  end
41
74
  self
42
75
  end
43
76
 
44
- # Defines a new option
45
- # @note
46
- # - It will override with a Warning options with same `short` or `name`
47
- def option(*args, **kargs, &block)
48
- opt = RakeCommander::Option.new(*args, **kargs, &block)
49
- add_to_options(opt)
50
- self
77
+ # It builds the `OptionParser` injecting the `middleware` block and parses `argv`
78
+ # @note this method is extended in via the following modules:
79
+ # 1. `RakeCommander::Options::Result`: makes the method to return a `Hash` with results,
80
+ # as well as captures/moves the **leftovers** to their own keyed argument.
81
+ # 2. `RakeCommander::Options:Error`: adds error handling (i.e. forward to rake commander errors)
82
+ # @param argv [Array<String>] the command line arguments to be parsed.
83
+ # @param method [Symbol] the parsing method (default is `:parse`; others: `:order`)
84
+ # @return [Array<String>] the **leftovers** of the `OptionParser#parse` call.
85
+ def parse_options(argv = ARGV, method: :parse, &middleware)
86
+ RakeCommander.rake_comm_debug "(#{name}) P A R S E O P T I O N S !", "\n", num: 5, pid: true
87
+ RakeCommander.rake_comm_debug " ---> ARGV: [#{argv.map {|a| a.nil?? "nil" : "'#{a}'"}.join(', ')}]"
88
+ options_parser(&middleware).send(method, argv)
51
89
  end
52
90
 
53
- # Overrides the auto-generated banner
54
- def banner(desc = :not_used)
55
- return @banner = desc unless desc == :not_used
56
- return @banner if @banner
57
- return task_options_banner if respond_to?(:task_options_banner, true)
91
+ # List of configured options
92
+ # @return [Array<RakeCommander::Option>]
93
+ def options
94
+ options_hash.values.uniq
58
95
  end
59
96
 
60
- # @return [Boolean] whether results should include options defined
61
- # with a default, regarless if they are invoked
62
- def results_with_all_defaults(value = nil)
63
- if value.nil?
64
- @results_with_all_defaults || false
65
- else
66
- @results_with_all_defaults = !!value
67
- end
97
+ # @return [Boolean] are there options defined?
98
+ def options?
99
+ !options.empty?
68
100
  end
69
101
 
70
- # It builds the `OptionParser` injecting the `middleware` block.
71
- # @return [Hash] with `short` option as `key` and final value as `value`.
72
- def parse_options(argv = ARGV, leftovers: [], &middleware)
73
- options_parser_with_results(middleware) do |options_parser|
74
- argv = pre_parse_arguments(argv, options: options_hash)
75
- pp argv
76
- leftovers.push(*options_parser.parse(argv))
77
- rescue OptionParser::MissingArgument => e
78
- raise RakeCommander::Options::MissingArgument, e, cause: nil
79
- rescue OptionParser::InvalidArgument => e
80
- error = RakeCommander::Options::InvalidArgument
81
- error = error.new(e)
82
- opt = options_hash[error.option_sym]
83
- msg = "missing required argument: #{opt&.name_hyphen} (#{opt&.short_hyphen})"
84
- raise RakeCommander::Options::MissingArgument, msg, cause: nil if opt&.argument_required?
85
- raise error, e, cause: nil
86
- end.tap do |results|
87
- check_required_presence!(results)
88
- end
102
+ # Clears all the options.
103
+ def clear_options!
104
+ @options_hash = {}
105
+ self
89
106
  end
90
107
 
91
108
  protected
92
109
 
110
+ # It allows to add a middleware block that is called during the parsing phase.
93
111
  # @return [OptionParser] the built options parser.
94
112
  def options_parser(&middleware)
95
113
  new_options_parser do |opts|
96
114
  opts.banner = banner if banner
97
- options.each {|opt| opt.add_switch(opts, &middleware)}
115
+ option_help(opts)
116
+ free_shorts = implicit_shorts
117
+
118
+ options.each do |opt|
119
+ free_short = free_shorts.key?(opt.short_implicit)
120
+ opt.add_switch(opts, implicit_short: free_short, &middleware)
121
+ end
98
122
  end
99
123
  end
100
124
 
125
+ # @return [OptionParser]
101
126
  def new_options_parser(&block)
102
127
  require 'optparse'
103
128
  OptionParser.new(&block)
104
129
  end
105
130
 
106
- private
107
-
108
- # Expects a block that should do the final call to `#parse`
109
- def options_parser_with_results(middleware)
110
- result_defaults.tap do |result|
111
- results_collector = proc do |value, default, short, name|
112
- middleware&.call(value, default, short, name)
113
- result[short] = value.nil?? default : value
114
- end
115
- options_parser = options_parser(&results_collector)
116
- yield(options_parser)
131
+ # The options indexed by the short and the name (so doubled up in the hash).
132
+ # @param with_implicit [Boolean] whether free implicit shorts of declared options should be included
133
+ # among the keys (pointing to the specific option that has it implicit).
134
+ # @return [Hash] with Symbol name and shorts as keys, and `RakeCommander::Option` as values.
135
+ def options_hash(with_implicit: false)
136
+ @options_hash ||= {}
137
+ return @options_hash unless with_implicit
138
+ @options_hash.merge(implicit_shorts)
139
+ end
140
+
141
+ # This covers the gap where `OptionParser` auto-generates shorts out of option names.
142
+ # @note `OptionParser` implicitly generates a short for the option name. When defining
143
+ # an option that uses this short, the short gets overriden/reused by the explicit option.
144
+ # Otherwise, the short is actually available for the former option, regarldess you
145
+ # specified a different short for it (i.e. both shorts, expicit and implicit, will work).
146
+ # @note for two options with same implicit free short, latest in order will take it, which
147
+ # is what `OptionParser` will actually do.
148
+ # @return [Hash] with free shorts that are implicit to some option
149
+ def implicit_shorts
150
+ options.each_with_object({}) do |opt, implicit|
151
+ short = opt.short_implicit
152
+ implicit[short] = opt unless options_hash.key?(short)
117
153
  end
118
154
  end
119
155
 
120
- # Based on `required` options, it sets the `default`
121
- def result_defaults
122
- {}.tap do |res_def|
123
- options.select do |opt|
124
- (results_with_all_defaults && opt.default?) \
125
- || (opt.required? && opt.default?)
126
- end.each do |opt|
127
- res_def[opt.short] = opt.default
128
- end
156
+ private
157
+
158
+ # Explicitly installs the help of the options
159
+ # @note `OptionParser` already has `-h --help` as a native option.
160
+ # @param opts [OptionParser] where the help will be added.
161
+ def option_help(opts)
162
+ return false if options_hash.key?(:help) || options_hash.key?(:h)
163
+ option(:h, :help, 'Prints this help') do
164
+ puts opts
165
+ exit(0)
129
166
  end
167
+ self
130
168
  end
131
169
 
132
- # It throws an exception if any of the required options
133
- # is missing in results
134
- def check_required_presence!(results)
135
- missing = options.select(&:required?).reject do |opt|
136
- results.key?(opt.short) || results.key?(opt.name)
170
+ # @return [Array<RakeCommander::Option>]
171
+ def to_options(value)
172
+ if value.is_a?(Class) && value <= self
173
+ value.options
174
+ elsif value.is_a?(self)
175
+ value.class.options
176
+ elsif value.is_a?(Array)
177
+ value.select {|opt| opt.is_a?(option_class)}
178
+ elsif value.is_a?(Hash)
179
+ to_options(value.values)
180
+ elsif value.is_a?(Enumerable) || value.respond_to?(:to_a)
181
+ to_options(value.to_a)
137
182
  end
138
- raise RakeCommander::Options::MissingOption, missing unless missing.empty?
139
183
  end
140
184
 
141
- def add_to_options(opt)
142
- if prev = options_hash[opt.short]
143
- puts "Warning: Overriding option with short '#{prev.short}' ('#{prev.name}')"
144
- options_hash.delete(prev.short)
145
- options_hash.delete(prev.name)
185
+ # Adds to `@options_hash` the option `opt`
186
+ # @todo add `exception` parameter, to trigger an exception
187
+ # when `opt` name or short are taken (and override is `false`)
188
+ # @todo allow to specif if `:tail`, `:top` or `:base` (default)
189
+ # @param opt [RakeCommander::Option] the option to be added.
190
+ # @param override [Boolean] whether we should continue, may this action override (an)other option(s).
191
+ # @return [RakeCommander::Option, NilClass] the option that was added, `nil` is returned otherwise.
192
+ def add_to_options(opt, override: true)
193
+ name_ref = respond_to?(:name)? " (#{name})" : ''
194
+ if sprev = options_hash[opt.short]
195
+ return nil unless override
196
+ puts "Warning#{name_ref}: Overriding option '#{sprev.name}' with short '#{sprev.short}' ('#{opt.name}')"
197
+ delete_from_options(sprev)
146
198
  end
147
- if prev = options_hash[opt.name]
148
- puts "Warning: Overriding option with name '#{prev.name}' ('#{prev.short}')"
149
- options_hash.delete(prev.short)
150
- options_hash.delete(prev.name)
199
+ if nprev = options_hash[opt.name]
200
+ return nil unless override
201
+ puts "Warning#{name_ref}: Overriding option '#{nprev.short}' with name '#{nprev.name}' ('#{opt.short}')"
202
+ delete_from_options(nprev)
151
203
  end
152
204
  options_hash[opt.name] = options_hash[opt.short] = opt
153
205
  end
154
- end
155
206
 
156
- def options(argv = ARGV)
157
- @options ||= self.class.parse_options(argv, leftovers: options_leftovers)
158
- end
207
+ # Deletes an option from the `options_hash`
208
+ # @param opt [RakeCommander::Option] the option to be deleted.
209
+ # @return [RakeCommander::Option, NilClass] the option that was deleted, or `nil` otherwise.
210
+ def delete_from_options(opt)
211
+ res = options_hash.delete(opt.short)
212
+ options_hash.delete(opt.name) || res
213
+ end
214
+
215
+ # Replaces option `ref` with option `opt`.
216
+ # @note this method was added aiming to keep the same position for an option we override.
217
+ # @param ref [RakeCommander::Option] the option to be replaced.
218
+ # @param opt [RakeCommander::Option] the option that will replace `ref`.
219
+ # @param override [Boolean] whether we should continue, may there be collateral override to other options.
220
+ # @return [RakeCommander::Option, NilClass] `opt` if it was added, or `nil` otherwise.
221
+ def replace_in_options(ref, opt, override: false)
222
+ # Try to keep the same potition
223
+ options_hash[ref.short] = options_hash[ref.name] = nil
224
+ add_to_options(opt, override: override).tap do |added_opt|
225
+ # restore previous status
226
+ next options_hash[ref.short] = options_hash[ref.name] = ref unless added_opt
227
+ delete_empty_keys(options_hash)
228
+ end
229
+ end
159
230
 
160
- def options_leftovers
161
- @options_leftovers ||= []
231
+ # Deletes all keys that have `nil` as value
232
+ def delete_empty_keys(hash)
233
+ hash.tap do |_h|
234
+ hash.dup.each do |k, v|
235
+ next unless v.nil?
236
+ hash.delete(k)
237
+ end
238
+ end
239
+ end
162
240
  end
163
241
  end
164
242
  end
165
243
 
166
- require_relative 'options/error'
167
244
  require_relative 'options/set'
@@ -0,0 +1,79 @@
1
+ # Patching `rake`
2
+
3
+ ## Patch Rational
4
+
5
+ Let's say that when we invoke `rake` from the command line, `rake-commander` is loaded from a `Rakefile` (i.e. `require 'rake-commander'`). Looking at the `Rake::Application#run` method code, this places the patch moment, at the best, during the load of the `Rakefile`; during execution of the `load_rakefile` private method ([here is the call](https://github.com/ruby/rake/blob/48e798484babf725b0562cc417986da513e5d0ae/lib/rake/application.rb#L82)).
6
+
7
+ ### Challenges encountered with the `rake` executable
8
+
9
+ Let's say you require/load `rake-commander` in a `Rakefile`, and invoke the [`rake` executable](https://github.com/ruby/rake/blob/master/exe/rake). By the time rake commander is loaded, `Rake` has already captured the `ARGV`, parsed its own options, and pre-parsed possible task(s) invokations.
10
+
11
+ The **two main problems** to deal with are:
12
+
13
+ 1. Rake has it's own `OptionsParser`. If any of your rake `task` options matches any of those, you will be unintentionally invoking `rake` functionality.
14
+ 2. Let's say you require/load `rake-commander` in a `Rakefile`. By the time rake commander is loaded, `rake` has already collected as `top_level_tasks` the arguments of your task options; so those that do not start with dash `-` ([see private method `collect_command_line_tasks` in `Rake::Application`](https://github.com/ruby/rake/blob/48e798484babf725b0562cc417986da513e5d0ae/lib/rake/application.rb#L782)).
15
+
16
+ This is also true when you invoke `rake` via _shell_ from within another task.
17
+
18
+ **Example**
19
+
20
+ Without the current patch, this is what was happening.
21
+
22
+ ```
23
+ $ raked examples:chainer -- --chain --say "Just saying..." --with raked
24
+ Calling --> 'rake examples:chained -- --say "Just saying..."'
25
+ Chained task has been called!!
26
+ Just saying...
27
+ rake aborted!
28
+ Don't know how to build task 'Just saying...' (See the list of available tasks with `rake --tasks`)
29
+ ```
30
+
31
+ ### The alternative of a `raked` executable
32
+
33
+ **`raked` executable is not necessary and is not provided for prod environments. The current patch allows to start directly from `rake`**.
34
+
35
+ * This has been kept to the only purpose of documentation.
36
+
37
+ The `raked` executable would be a modified version of the `rake` executable, where `rake_commander` is loaded right after requiring `rake` and before `Rake.application.run` is invoked.
38
+
39
+ ```ruby
40
+ #!/usr/bin/env ruby
41
+
42
+ require "rake"
43
+ require "rake-commander"
44
+ Rake.application.run
45
+ ```
46
+
47
+ This would allow the patch to be active right at the beginning, preventing this way the patch to kick in after the `rake` application has been firstly launched (it saves to rake one loop of parsing arguments and loading rake files).
48
+
49
+ ```
50
+ $ raked examples:chainer -- --chain --say "Just saying..." --with raked
51
+ Calling --> 'bin\raked examples:chained -- --say "Just saying..."'
52
+ Chained task has been called!!
53
+ Just saying...
54
+ ```
55
+
56
+ Using `raked` as separate namespace vs `rake` is completely optional. Most will prefer to keep on just with the main `rake` executable and `rake-commander` as enhancement to it. This is the rational behind the second patch (explained in detail in the next section).
57
+
58
+
59
+ ## Reload `Rake` application
60
+
61
+ The target is to be able to use `rake` indistinctly (rather than having to rewrite rake commands as `raked`). Unfortunately the **only way around** to the _application-has-started_ is to just **relaunch/reload the application** when the patch kicks in (wouldn't know how to and shouldn't try to reuse the current running application: i.e. task options parsed as rake option modifiers that have already done some stuff).
62
+
63
+ Fortunately, the target of `rake-commander` is just to **enhance** existing syntax, which gives a very specific target when it comes to **patching**. The key factor to reach a clean patch is to design the syntax in a fixed way where there is no much flexibility but clearly stated delimiters (i.e. no fancy guessing where dependencies are introduced on defined task options).
64
+
65
+ Relaunching the application to a new instance requires very little:
66
+
67
+ ```ruby
68
+ Rake.application = Rake::Application.new
69
+ Rake.application.run
70
+ exit(0) # abort previous application run
71
+ ```
72
+
73
+ ## Missing tasks on reload
74
+
75
+ Relaunching the `rake` application entails issues with `require` in the chain of `Rakefile` files that have already been loaded. Apparently some tasks of some gems are installed during the `require` runtime, rather than explicitly declaring them in the rake file.
76
+
77
+ This is the case for `bundler/gem_tasks` (i.e. `require "bundler/gem_tasks"`), where all these `tasks` will be missing: build, build:checksum, clean, clobber, install, install:local, release, release:guard_clean, release:rubygem_push, release:source_control_push.
78
+
79
+ It can potentially be looked at, if ever this shows up to new review.
@@ -0,0 +1,46 @@
1
+ class RakeCommander
2
+ module Patcher
3
+ module Application
4
+ module RunMethod
5
+ include RakeCommander::Patcher::Base
6
+
7
+ class << self
8
+ def target
9
+ Rake::Application
10
+ end
11
+
12
+ def patch_prepend(_invoked_by)
13
+ return unless target_defined?
14
+ Rake::Application.prepend Patch
15
+ end
16
+
17
+ def target_defined?
18
+ return true if defined?(target)
19
+ puts "Warning (#{self}): undefined target #{target}"
20
+ false
21
+ end
22
+ end
23
+
24
+ module Patch
25
+ include RakeCommander::Patcher::Debug
26
+
27
+ # To extend the command line syntax we need to patch `Rake`, provided that
28
+ # this gem's extended options are not in `argv` when `Rake` processes it.
29
+ # @note we define an instance variable so we can know if the patch was applied when it started.
30
+ # @note This patch only works fine if `Rake::Application#run` is **invoked after****
31
+ # **`RakeCommander` has been required**.
32
+ # * So by itself alone it allows to use `raked` executable that this gem provides.
33
+ def run(argv = ARGV)
34
+ @rake_commander_run_argv_patch = true unless instance_variable_defined?(:@rake_commander_run_argv_patch)
35
+ RakeCommander.self_load
36
+ rake_comm_debug "R U N !", "\n", num: 1, pid: true
37
+ rake_comm_debug " ---> Command: #{$PROGRAM_NAME}"
38
+ rake_comm_debug " ---> ARGV: [#{argv.map {|a| "'#{a}'"}.join(', ')}]"
39
+ argv = RakeCommander.argv_rake_native_arguments(argv)
40
+ super(argv)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ class RakeCommander
2
+ module Patcher
3
+ module Application
4
+ module TopLevelMethod
5
+ include RakeCommander::Patcher::Base
6
+
7
+ class << self
8
+ def target
9
+ Rake::Application
10
+ end
11
+
12
+ def patch_prepend(_invoked_by)
13
+ return unless target_defined?
14
+ Rake::Application.prepend Patch
15
+ end
16
+
17
+ def target_defined?
18
+ return true if defined?(target)
19
+ puts "Warning (#{self}): undefined target #{target}"
20
+ false
21
+ end
22
+ end
23
+
24
+ module Patch
25
+ include RakeCommander::Patcher::Debug
26
+
27
+ # To preserve `rake` as main executable, as the `RunMethod::Patch` is applied only
28
+ # when `Rake::Application` requires the `Rakefile` that loads `rake-commander`,
29
+ # we need to:
30
+ # 1. Intercept the execution on the next stage of the `Rake::Application#run` command,
31
+ # [the `top_level` call](https://github.com/ruby/rake/blob/48e798484babf725b0562cc417986da513e5d0ae/lib/rake/application.rb#L82),
32
+ # and **re-launch** the rake application (so it only receives the `ARGV` cut that the main patch provides)
33
+ # 2. Ensure that **re-launch** is done only once.
34
+ # 3. Ensure that it does `exit(0)` to the original running application.
35
+ def top_level
36
+ unless @rake_commander_run_argv_patch
37
+ @rake_commander_run_argv_patch = true
38
+ RakeCommander.relaunch_rake_application
39
+ # Should not reach this point
40
+ end
41
+ rake_comm_debug "T O P L E V E L ( p a t c h a c t i v e )", "\n", num: 2, pid: true
42
+ rake_comm_debug " ---> Known tasks: #{tasks.map(&:name).join(", ")}"
43
+ super
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+ include RakeCommander::Patcher::Debug
49
+
50
+ # Reloading `Rakefile` has drawbacks around `require` only being launched once per
51
+ # dependency. Apparently some tasks of some gems are installed at `require` run-time.
52
+ # This requires to keep known tasks when we switch the application.
53
+ def relaunch_rake_application
54
+ prev_rake_app = Rake.application
55
+ rake_comm_debug "R A K E R E L A U N C H ( p a t c h i n a c t i v e )", "\n", num: 2, pid: true
56
+ rake_comm_debug " ---> Known tasks: #{prev_rake_app.tasks.map(&:name).join(", ")}"
57
+ Rake.application = Rake::Application.new
58
+ rake_comm_debug "N e w R a k e A p p", "\n", num: 4, pid: true
59
+ RakeCommander.self_load_reset
60
+ Rake.application.run #RakeCommander.argv_rake_native_arguments(ARGV)
61
+ rake_comm_debug "T e r m i n a t i n g R U N", "\n", num: 3, pid: true
62
+ exit(0)
63
+ end
64
+
65
+ private
66
+
67
+ def rake_reparse_argv(argv = ARGV)
68
+ RakeCommander.argv_rake_native_arguments(argv)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,16 @@
1
+ class RakeCommander
2
+ module Patcher
3
+ module Application
4
+ include RakeCommander::Patcher::Base
5
+ require_relative 'application/run_method'
6
+ require_relative 'application/top_level_method'
7
+
8
+ class << self
9
+ def patch_include(base)
10
+ base.send :include, RunMethod
11
+ base.send :include, TopLevelMethod
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,45 @@
1
+ class RakeCommander
2
+ module Patcher
3
+ # Base of self-applied patchers.
4
+ # @note a patcher will be applied when it's included.
5
+ module Base
6
+ class << self
7
+ def included(base)
8
+ super(base)
9
+ base.extend ClassMethods
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ def included(base)
15
+ super(base)
16
+ base.extend self::ClassMethods if defined?(self::ClassMethods)
17
+ invoke_patch_methods!(base) unless self == RakeCommander::Patcher::Base
18
+ end
19
+
20
+ def invoke_patch_methods!(base)
21
+ raise "#{self}: no patch methods. Patching with include requires at least one." unless any_patch_method?
22
+ patch_prepend(base) if patch_prepend?
23
+ patch_include(base) if patch_include?
24
+ patch_extend(base) if patch_extend?
25
+ end
26
+
27
+ def any_patch_method?
28
+ patch_prepend? || patch_include? || patch_extend?
29
+ end
30
+
31
+ def patch_prepend?
32
+ respond_to?(:patch_prepend)
33
+ end
34
+
35
+ def patch_include?
36
+ respond_to?(:patch_include)
37
+ end
38
+
39
+ def patch_extend?
40
+ respond_to?(:patch_extend)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ class RakeCommander
2
+ module Patcher
3
+ # Helpers to patch
4
+ module Debug
5
+ # Helper for debugging
6
+ def rake_comm_debug(msg, prefix = '', num: nil, pid: false)
7
+ return unless RakeCommander::Patcher.debug?
8
+ rake_comm_debug_random_object_id
9
+ num = num ? "#{num}. " : nil
10
+ if pid
11
+ meta = "(PID: #{Process.pid} ++ Thread: #{Thread.current.object_id} ++ Ruby 'main': #{rake_comm_debug_main_object_id})"
12
+ msg = "#{prefix}( #{num}#{Rake.application.object_id}) #{msg} #{meta}"
13
+ elsif num
14
+ msg = "#{prefix}( #{num}) #{msg} "
15
+ end
16
+ puts msg
17
+ end
18
+
19
+ def rake_comm_debug_main_object_id
20
+ eval('self.object_id', TOPLEVEL_BINDING, __FILE__, __LINE__)
21
+ end
22
+
23
+ def rake_comm_debug_random_object_id
24
+ return false if @rake_comm_debug_random_object_id
25
+ @rake_comm_debug_random_object_id = Array(1..20).sample.times.map do |i|
26
+ i.to_s.tap(&:object_id)
27
+ end
28
+ true
29
+ end
30
+ end
31
+ end
32
+ end