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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.rubocop.yml +12 -8
- data/CHANGELOG.md +69 -4
- data/LICENSE +21 -0
- data/README.md +94 -2
- data/Rakefile +11 -13
- data/examples/01_basic_example.rb +28 -0
- data/examples/02_a_chainer_example.rb +66 -0
- data/examples/02_a_chainer_options_set.rb +8 -0
- data/examples/02_b_chained_example.rb +13 -0
- data/examples/03_a_chainer_plus_example.rb +34 -0
- data/examples/03_b_chained_plus_example.rb +17 -0
- data/examples/Examples.rake +7 -0
- data/examples/README.md +79 -0
- data/examples/libs/shell_helpers.rb +81 -0
- data/lib/rake-commander/base/class_auto_loader.rb +45 -7
- data/lib/rake-commander/base/class_helpers.rb +16 -61
- data/lib/rake-commander/base/class_inheritable.rb +122 -0
- data/lib/rake-commander/base/custom_error.rb +52 -0
- data/lib/rake-commander/base/object_helpers.rb +42 -0
- data/lib/rake-commander/base.rb +16 -2
- data/lib/rake-commander/option.rb +115 -25
- data/lib/rake-commander/options/arguments.rb +206 -94
- data/lib/rake-commander/options/description.rb +17 -0
- data/lib/rake-commander/options/error/base.rb +86 -0
- data/lib/rake-commander/options/error/handling.rb +106 -0
- data/lib/rake-commander/options/error/invalid_argument.rb +21 -0
- data/lib/rake-commander/options/error/invalid_option.rb +9 -0
- data/lib/rake-commander/options/error/missing_argument.rb +10 -0
- data/lib/rake-commander/options/error/missing_option.rb +48 -0
- data/lib/rake-commander/options/error/unknown_argument.rb +32 -0
- data/lib/rake-commander/options/error.rb +75 -10
- data/lib/rake-commander/options/name.rb +67 -23
- data/lib/rake-commander/options/result.rb +107 -0
- data/lib/rake-commander/options/set.rb +7 -1
- data/lib/rake-commander/options.rb +175 -98
- data/lib/rake-commander/patcher/README.md +79 -0
- data/lib/rake-commander/patcher/application/run_method.rb +46 -0
- data/lib/rake-commander/patcher/application/top_level_method.rb +74 -0
- data/lib/rake-commander/patcher/application.rb +16 -0
- data/lib/rake-commander/patcher/base.rb +45 -0
- data/lib/rake-commander/patcher/debug.rb +32 -0
- data/lib/rake-commander/patcher/helpers.rb +44 -0
- data/lib/rake-commander/patcher.rb +26 -0
- data/lib/rake-commander/rake_context/wrapper.rb +2 -0
- data/lib/rake-commander/rake_task.rb +49 -54
- data/lib/rake-commander/version.rb +1 -1
- data/lib/rake-commander.rb +4 -0
- data/rake-commander.gemspec +4 -1
- metadata +74 -6
- data/examples/basic.rb +0 -30
- 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.
|
13
|
-
base.
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
27
|
-
|
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
|
-
|
31
|
-
|
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
|
37
|
-
|
38
|
-
|
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
|
-
#
|
45
|
-
# @note
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
#
|
54
|
-
|
55
|
-
|
56
|
-
|
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]
|
61
|
-
|
62
|
-
|
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
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
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
|
-
|
107
|
-
|
108
|
-
#
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
-
|
157
|
-
@
|
158
|
-
|
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
|
-
|
161
|
-
|
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
|