rake-commander 0.1.2 → 0.2.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +8 -0
  3. data/.rubocop.yml +14 -8
  4. data/CHANGELOG.md +84 -4
  5. data/Gemfile +1 -1
  6. data/LICENSE +21 -0
  7. data/README.md +95 -3
  8. data/Rakefile +11 -13
  9. data/examples/01_basic_example.rb +28 -0
  10. data/examples/02_a_chainer_example.rb +66 -0
  11. data/examples/02_a_chainer_options_set.rb +8 -0
  12. data/examples/02_b_chained_example.rb +13 -0
  13. data/examples/03_a_chainer_plus_example.rb +34 -0
  14. data/examples/03_b_chained_plus_example.rb +17 -0
  15. data/examples/Examples.rake +7 -0
  16. data/examples/README.md +79 -0
  17. data/examples/libs/shell_helpers.rb +81 -0
  18. data/lib/rake-commander/base/class_auto_loader.rb +45 -7
  19. data/lib/rake-commander/base/class_helpers.rb +16 -61
  20. data/lib/rake-commander/base/class_inheritable.rb +122 -0
  21. data/lib/rake-commander/base/custom_error.rb +52 -0
  22. data/lib/rake-commander/base/object_helpers.rb +42 -0
  23. data/lib/rake-commander/base.rb +16 -2
  24. data/lib/rake-commander/option.rb +115 -25
  25. data/lib/rake-commander/options/arguments.rb +206 -94
  26. data/lib/rake-commander/options/description.rb +17 -0
  27. data/lib/rake-commander/options/error/base.rb +86 -0
  28. data/lib/rake-commander/options/error/handling.rb +106 -0
  29. data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
  30. data/lib/rake-commander/options/error/invalid_option.rb +9 -0
  31. data/lib/rake-commander/options/error/missing_argument.rb +10 -0
  32. data/lib/rake-commander/options/error/missing_option.rb +48 -0
  33. data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
  34. data/lib/rake-commander/options/error.rb +75 -10
  35. data/lib/rake-commander/options/name.rb +67 -23
  36. data/lib/rake-commander/options/result.rb +107 -0
  37. data/lib/rake-commander/options/set.rb +7 -1
  38. data/lib/rake-commander/options.rb +175 -102
  39. data/lib/rake-commander/patcher/README.md +79 -0
  40. data/lib/rake-commander/patcher/application/run_method.rb +46 -0
  41. data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
  42. data/lib/rake-commander/patcher/application.rb +16 -0
  43. data/lib/rake-commander/patcher/base.rb +45 -0
  44. data/lib/rake-commander/patcher/debug.rb +32 -0
  45. data/lib/rake-commander/patcher/helpers.rb +44 -0
  46. data/lib/rake-commander/patcher.rb +26 -0
  47. data/lib/rake-commander/rake_context/wrapper.rb +2 -0
  48. data/lib/rake-commander/rake_task.rb +50 -50
  49. data/lib/rake-commander/version.rb +1 -1
  50. data/lib/rake-commander.rb +4 -0
  51. data/rake-commander.gemspec +5 -2
  52. metadata +75 -7
  53. data/examples/basic.rb +0 -30
  54. 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,164 +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
- left_overs = []
74
- options_parser_with_results(middleware) do |options_parser|
75
- argv = pre_parse_arguments(argv, options_hash)
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
- if (opt = options_hash[error.option_sym]) && opt.argument_required?
83
- msg = "missing required argument: #{opt.name_hyphen} (#{opt.short_hyphen})"
84
- raise RakeCommander::Options::MissingArgument, msg, cause: nil
85
- else
86
- raise error, e, cause: nil
87
- end
88
- end.tap do |results|
89
- check_required_presence!(results)
90
- end
102
+ # Clears all the options.
103
+ def clear_options!
104
+ @options_hash = {}
105
+ self
91
106
  end
92
107
 
93
108
  protected
94
109
 
110
+ # It allows to add a middleware block that is called during the parsing phase.
95
111
  # @return [OptionParser] the built options parser.
96
112
  def options_parser(&middleware)
97
113
  new_options_parser do |opts|
98
114
  opts.banner = banner if banner
99
- 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
100
122
  end
101
123
  end
102
124
 
125
+ # @return [OptionParser]
103
126
  def new_options_parser(&block)
104
127
  require 'optparse'
105
128
  OptionParser.new(&block)
106
129
  end
107
130
 
108
- private
109
-
110
- # Expects a block that should do the final call to `#parse`
111
- def options_parser_with_results(middleware)
112
- result_defaults.tap do |result|
113
- results_collector = proc do |value, default, short, name|
114
- middleware&.call(value, default, short, name)
115
- result[short] = value.nil?? default : value
116
- end
117
- options_parser = options_parser(&results_collector)
118
- 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)
119
153
  end
120
154
  end
121
155
 
122
- # Based on `required` options, it sets the `default`
123
- def result_defaults
124
- {}.tap do |res_def|
125
- options.select do |opt|
126
- (results_with_all_defaults && opt.default?) \
127
- || (opt.required? && opt.default?)
128
- end.each do |opt|
129
- res_def[opt.short] = opt.default
130
- 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)
131
166
  end
167
+ self
132
168
  end
133
169
 
134
- # It throws an exception if any of the required options
135
- # is missing in results
136
- def check_required_presence!(results)
137
- missing = options.select do |opt|
138
- opt.required?
139
- end.reject do |opt|
140
- 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)
141
182
  end
142
- raise RakeCommander::Options::MissingOption.new(missing) unless missing.empty?
143
183
  end
144
184
 
145
- def add_to_options(opt)
146
- if prev = options_hash[opt.short]
147
- puts "Warning: Overriding option with short '#{prev.short}' ('#{prev.name}')"
148
- options_hash.delete(prev.short)
149
- 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)
150
198
  end
151
- if prev = options_hash[opt.name]
152
- puts "Warning: Overriding option with name '#{prev.name}' ('#{prev.short}')"
153
- options_hash.delete(prev.short)
154
- 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)
155
203
  end
156
204
  options_hash[opt.name] = options_hash[opt.short] = opt
157
205
  end
158
- end
159
206
 
160
- def options(argv = ARGV)
161
- @options ||= self.class.parse_options(argv, leftovers: self.options_leftovers)
162
- 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
163
230
 
164
- def options_leftovers
165
- @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
166
240
  end
167
241
  end
168
242
  end
169
243
 
170
- require_relative 'options/error'
171
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