grably 0.0.1
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 +7 -0
- data/LICENSE.txt +202 -0
- data/README.md +2 -0
- data/exe/grably +2 -0
- data/lib/ext/class.rb +28 -0
- data/lib/grably.rb +83 -0
- data/lib/grably/cli.rb +46 -0
- data/lib/grably/core.rb +15 -0
- data/lib/grably/core/app/enchancer.rb +36 -0
- data/lib/grably/core/application.rb +8 -0
- data/lib/grably/core/colors.rb +86 -0
- data/lib/grably/core/commands.rb +23 -0
- data/lib/grably/core/commands/cp.rb +103 -0
- data/lib/grably/core/commands/digest.rb +12 -0
- data/lib/grably/core/commands/log.rb +19 -0
- data/lib/grably/core/commands/run.rb +85 -0
- data/lib/grably/core/commands/serialize.rb +16 -0
- data/lib/grably/core/configuration.rb +39 -0
- data/lib/grably/core/configuration/pretty_print.rb +22 -0
- data/lib/grably/core/digest.rb +93 -0
- data/lib/grably/core/dsl.rb +15 -0
- data/lib/grably/core/essentials.rb +49 -0
- data/lib/grably/core/module.rb +64 -0
- data/lib/grably/core/product.rb +301 -0
- data/lib/grably/core/task.rb +30 -0
- data/lib/grably/core/task/bucket.rb +29 -0
- data/lib/grably/core/task/enchancer.rb +50 -0
- data/lib/grably/core/task/expand.rb +15 -0
- data/lib/grably/core/task/jobs.rb +58 -0
- data/lib/grably/job.rb +28 -0
- data/lib/grably/job/class.rb +93 -0
- data/lib/grably/job/exceptions.rb +0 -0
- data/lib/grably/job/instance.rb +159 -0
- data/lib/grably/job/manifest.rb +67 -0
- data/lib/grably/jobs.rb +4 -0
- data/lib/grably/jobs/sync.rb +91 -0
- data/lib/grably/jobs/text.rb +4 -0
- data/lib/grably/jobs/text/erb.rb +40 -0
- data/lib/grably/jobs/text/json.rb +12 -0
- data/lib/grably/jobs/text/text.rb +21 -0
- data/lib/grably/jobs/text/yaml.rb +12 -0
- data/lib/grably/jobs/unzip.rb +1 -0
- data/lib/grably/jobs/upload.rb +1 -0
- data/lib/grably/jobs/zip.rb +2 -0
- data/lib/grably/jobs/zip/unzip.rb +24 -0
- data/lib/grably/jobs/zip/zip.rb +46 -0
- data/lib/grably/runner.rb +31 -0
- data/lib/grably/server.rb +83 -0
- data/lib/grably/utils/pretty_printer.rb +63 -0
- data/lib/grably/version.rb +12 -0
- metadata +164 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'rake/application'
|
2
|
+
module Grably
|
3
|
+
# Methods for working with submodules
|
4
|
+
module Module
|
5
|
+
# List of file names we can load as submodule entry
|
6
|
+
DEFAULT_FILES = Rake::Application::DEFAULT_RAKEFILES
|
7
|
+
|
8
|
+
# Reference to external module task call
|
9
|
+
class ModuleCall
|
10
|
+
attr_reader :path, :profile, :task
|
11
|
+
# Initializes module reference with load path and profile
|
12
|
+
# @param [String] path absolute path to referencing module
|
13
|
+
def initialize(path, task, profile = c.profile)
|
14
|
+
@path = path
|
15
|
+
@task = task
|
16
|
+
@profile = profile
|
17
|
+
end
|
18
|
+
|
19
|
+
# Updates profile settings in module ref
|
20
|
+
# @param [*String] profile profile names
|
21
|
+
def with_profile(*profile)
|
22
|
+
@profile = profile
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def pretty_print
|
27
|
+
profiles = [*profile].flatten.join(', ')
|
28
|
+
"Call Grably[#{path} / #{profiles}] #{task.to_s.white.bright}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
# Get submodule object
|
34
|
+
# @param [String] path relative path to sumbodule
|
35
|
+
# @return [Grably::Module::ModuleCall] addresed submodule
|
36
|
+
def reference(path, task)
|
37
|
+
base_path = File.expand_path(File.join(Dir.pwd, path))
|
38
|
+
raise "#{path} does not exist" unless File.exist?(path)
|
39
|
+
path = if File.directory?(base_path)
|
40
|
+
ensure_module_dir(base_path)
|
41
|
+
else
|
42
|
+
ensure_filename(base_path)
|
43
|
+
end
|
44
|
+
ModuleCall.new(path, task)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ensures that provided path points to one of allowed to load files
|
48
|
+
def ensure_filename(path)
|
49
|
+
basename = File.basename(path)
|
50
|
+
return path if DEFAULT_FILES.include?(basename)
|
51
|
+
|
52
|
+
exp = DEFAULT_FILES.join(', ')
|
53
|
+
raise "Wrong file name #{basename} expected one of #{exp}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def ensure_module_dir(path)
|
57
|
+
base_path = Dir["#{path}/*"]
|
58
|
+
.find { |f| DEFAULT_FILES.include?(File.basename(f)) }
|
59
|
+
raise "Can't find any file to load in #{path}" unless base_path
|
60
|
+
base_path
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,301 @@
|
|
1
|
+
require 'powerpack/string/format' # Adds format method
|
2
|
+
require 'powerpack/string/remove_prefix'
|
3
|
+
require_relative 'task'
|
4
|
+
|
5
|
+
module Grably
|
6
|
+
module Core
|
7
|
+
class Product
|
8
|
+
# Class stub for future reference.
|
9
|
+
# Main implementation goes below
|
10
|
+
end
|
11
|
+
|
12
|
+
# Set of predefined product filters, used by ProductExpand
|
13
|
+
module ProductFilter
|
14
|
+
# Generates lambda filter out of `String` with
|
15
|
+
# glob pattern description
|
16
|
+
def generate_glob_filter(glob)
|
17
|
+
# Negative glob starts with '!'
|
18
|
+
negative = glob.start_with?('!')
|
19
|
+
# Strip leading '!' then
|
20
|
+
glob.remove_prefix!('!') if negative
|
21
|
+
lambda do |path|
|
22
|
+
# TODO: I'm pretty sure that someone would like to have set_no_extglob, set_no_dotmatch, set_no_pathname
|
23
|
+
# TODO: as global methods for ProductFilter module
|
24
|
+
matches = File.fnmatch(glob, path, File::FNM_EXTGLOB | File::FNM_DOTMATCH | File::FNM_PATHNAME)
|
25
|
+
# inverse match if glob is negative
|
26
|
+
matches = !matches if negative
|
27
|
+
matches
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Matches filter string
|
32
|
+
FILTER_STRING_REGEXP = /^((((?<new_base>.+?)?:)?((?<old_base>.+?)?:))?)(?<glob>.+)$/
|
33
|
+
# FILTER_STRING_REGEXP groups in fixed order.
|
34
|
+
FILTER_STRING_GROUPS = %i(new_base old_base glob).freeze
|
35
|
+
|
36
|
+
def generate_string_filter(filter_string)
|
37
|
+
new_base, old_base, glob = parse_string_filter(filter_string)
|
38
|
+
glob_filter = generate_glob_filter(glob)
|
39
|
+
lambda do |products, _expand|
|
40
|
+
filtered = filter_products(products, new_base, old_base) { |_src, dst, _meta| glob_filter.call(dst) }
|
41
|
+
filtered.map { |src, dst, meta| Product.new(src, dst, meta) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def parse_string_filter(filter_string)
|
46
|
+
# Here we need generate lambda for string filter
|
47
|
+
parsed_filter = FILTER_STRING_REGEXP.match(filter_string) do |m|
|
48
|
+
FILTER_STRING_GROUPS.map { |g| m[g] }
|
49
|
+
end
|
50
|
+
parsed_filter || raise('Filter \'%s\' doesn\'t match format'.format(filter_string))
|
51
|
+
end
|
52
|
+
|
53
|
+
def filter_products(products, new_base, old_base, &dst_filter)
|
54
|
+
products
|
55
|
+
.map { |p| [p.src, p.dst, p.meta] }
|
56
|
+
.select { |_, dst, _| !old_base || dst.start_with?(old_base) }
|
57
|
+
.map { |src, dst, meta| [src, dst.gsub(%r{^#{old_base.to_s}[/\\]}, ''), meta] }
|
58
|
+
.select(&dst_filter)
|
59
|
+
.map { |src, dst, meta| [src, new_base.nil? ? dst : File.join(new_base, dst), meta] }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Product expansion rules.
|
64
|
+
# Expand is mapping from anything(almost) to list of products
|
65
|
+
#
|
66
|
+
# We have following expansion rules:
|
67
|
+
#
|
68
|
+
# * Symbol can represent following entities:
|
69
|
+
#
|
70
|
+
# * `:task_deps` - all all backets from all task prerequisites
|
71
|
+
# * `:last_job` - result of last executed job
|
72
|
+
# * any other symbol - backet of task with that name
|
73
|
+
#
|
74
|
+
# * Hash describes mapping from product expandable expression to filter in form of `{ expr => filter }`.
|
75
|
+
# Filter can be either string or lambda (proc).
|
76
|
+
# * If filter is a String it can be:
|
77
|
+
# * glob pattern (`gN`)
|
78
|
+
# * negative glob pattern, e.g. `!` is prepended (`!gN`)
|
79
|
+
# * filter with substitution (can be in two forms):
|
80
|
+
# * `base:glob` - select every file starting with `base`, strip `base`
|
81
|
+
# from beginning, apply glob to the rest of path
|
82
|
+
# * `new_base:old_base:glob` - select every file starting with base,
|
83
|
+
# strip `old_base`, apply glob to the rest of path, add `new_base`
|
84
|
+
# Every operation is performed over Product `dst`, not `src`.
|
85
|
+
# `glob` means any glob (either simple glob or negative glob)
|
86
|
+
#
|
87
|
+
# * If filter is `Proc`:
|
88
|
+
#
|
89
|
+
# * this proc must have arity of 2
|
90
|
+
# * first argument is expanded expression `expr`
|
91
|
+
# * second argument is contexted expand function (which means it have proper task argument)
|
92
|
+
#
|
93
|
+
# * `Array` expansion is expanding each element
|
94
|
+
#
|
95
|
+
# * `String` expands as glob pattern
|
96
|
+
# If string is path to file, then it expands into single element array of products.
|
97
|
+
# Else (i.e. string is directory) it expands into array of products representing
|
98
|
+
# directory content.
|
99
|
+
#
|
100
|
+
# * {Grably::Core::Task} expands to its backet.
|
101
|
+
# Behaving strictly we can only get task backet of
|
102
|
+
# self, i.e. target_task == context_task, or if target_task is
|
103
|
+
# context_task prerequisite (direct or indirect).
|
104
|
+
#
|
105
|
+
# * Product expands to self
|
106
|
+
module ProductExpand
|
107
|
+
class << self
|
108
|
+
include ProductFilter
|
109
|
+
|
110
|
+
def expand(srcs, task = nil)
|
111
|
+
# Wrap o in Array to simplify processing flow
|
112
|
+
srcs = [srcs] unless srcs.is_a? Array
|
113
|
+
# First typed expand will be array expand. So we will get array as
|
114
|
+
# result
|
115
|
+
typed_expand(srcs, task)
|
116
|
+
end
|
117
|
+
|
118
|
+
def expand_symbol(symbol, task)
|
119
|
+
case symbol
|
120
|
+
when :task_deps
|
121
|
+
typed_expand(task.prerequisites.map(&:to_sym), task)
|
122
|
+
else
|
123
|
+
task_ref = Task[symbol]
|
124
|
+
typed_expand(task_ref, task)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def expand_hash(hash, task)
|
129
|
+
hash.flat_map do |expr, filter|
|
130
|
+
# If got string generate lambda representing filter operation
|
131
|
+
filter = generate_string_filter(filter) if filter.is_a? String
|
132
|
+
raise 'Filter is not a proc %s'.format(filter) unless filter.is_a?(Proc)
|
133
|
+
filter.call(typed_expand(expr, task), ->(o) { expand(o, task) })
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def expand_array(elements, task)
|
138
|
+
elements.flat_map { |e| typed_expand(e, task) }
|
139
|
+
end
|
140
|
+
|
141
|
+
def expand_string(expr, _task)
|
142
|
+
unless File.exist?(expr)
|
143
|
+
warn "'#{expr}' does not exist. Can't expand path"
|
144
|
+
return []
|
145
|
+
end
|
146
|
+
# Will expand recursively over directory content.
|
147
|
+
if File.directory?(expr)
|
148
|
+
expand_dir(expr)
|
149
|
+
else
|
150
|
+
Product.new(expr)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def expand_proc(proc, task)
|
155
|
+
proc.call(->(o) { expand(o, task) })
|
156
|
+
end
|
157
|
+
|
158
|
+
def expand_task(target_task, context_task)
|
159
|
+
# Behaving strictly we can only get task backet of
|
160
|
+
# self, i.e. target_task == context_task, or if target_task is
|
161
|
+
# context_task prerequisite (direct or indirect).
|
162
|
+
unless check_dependencies(target_task, context_task)
|
163
|
+
raise(ArgumentError,
|
164
|
+
'Target task [%s] is not in context task [%s] prerequisites'
|
165
|
+
.format(target_task.name, context_task.name))
|
166
|
+
end
|
167
|
+
target_task.bucket
|
168
|
+
end
|
169
|
+
|
170
|
+
def expand_product(product, _)
|
171
|
+
product
|
172
|
+
end
|
173
|
+
|
174
|
+
# We define method table for expand rules.
|
175
|
+
# Key is object class, value is method.
|
176
|
+
#
|
177
|
+
# Initially we just define set of Class to Symbol mappings. Then calling
|
178
|
+
# ProductExpand.singleton_method on each method name.
|
179
|
+
# As bonus following this technique we will get NameError
|
180
|
+
# immediately after module load.
|
181
|
+
METHOD_TABLE =
|
182
|
+
Hash[*{
|
183
|
+
Symbol => :expand_symbol,
|
184
|
+
Hash => :expand_hash,
|
185
|
+
Array => :expand_array,
|
186
|
+
String => :expand_string,
|
187
|
+
Proc => :expand_proc,
|
188
|
+
Grably::Core::Task => :expand_task,
|
189
|
+
Product => :expand_product
|
190
|
+
}
|
191
|
+
.flat_map { |k, v| [k, ProductExpand.singleton_method(v)] }]
|
192
|
+
.freeze
|
193
|
+
|
194
|
+
def typed_expand(element, task)
|
195
|
+
# Fetching expand rule for element type
|
196
|
+
method_refs = METHOD_TABLE.select { |c, _| element.is_a?(c) }
|
197
|
+
raise 'Multiple expands found for %s. Expands %s'.format(element, method_refs.keys) if method_refs.size > 1
|
198
|
+
method_ref = method_refs.values.first
|
199
|
+
unless method_ref
|
200
|
+
err = 'No expand for type: %s. Element is \'%s\''
|
201
|
+
raise err.format(element.class, element)
|
202
|
+
end
|
203
|
+
# Every expand_%something% method follows same contract, so we just
|
204
|
+
# passing standard set of arguments
|
205
|
+
method_ref.call(element, task)
|
206
|
+
end
|
207
|
+
|
208
|
+
## Utility methods
|
209
|
+
|
210
|
+
# Ensure that target task is among context_task dependencies
|
211
|
+
def check_dependencies(target_task, context_task)
|
212
|
+
return true if target_task == context_task
|
213
|
+
context_task.all_prerequisite_tasks.include?(target_task)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def expand_dir(expr)
|
219
|
+
glob = File.join(expr, '**/*')
|
220
|
+
Dir[glob]
|
221
|
+
.select { |entry| File.file?(entry) }
|
222
|
+
.map { |file| Product.new(file, file.sub(expr + File::SEPARATOR, '')) }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
# Product is core, minimal entity in build process.
|
228
|
+
# It describes real file with virtual destination.
|
229
|
+
# Product instances should be immutable.
|
230
|
+
class Product
|
231
|
+
attr_reader :src, :dst, :meta
|
232
|
+
|
233
|
+
def initialize(src, dst = nil, meta = {})
|
234
|
+
raise 'src should be a string' unless src.is_a?(String)
|
235
|
+
raise 'dst should be a string' unless dst.is_a?(String) || dst.nil?
|
236
|
+
@src = File.expand_path(src)
|
237
|
+
@dst = dst || File.basename(src)
|
238
|
+
@meta = meta.freeze # Ensure meta is immutable
|
239
|
+
end
|
240
|
+
|
241
|
+
def [](*keys)
|
242
|
+
return @meta[keys.first] if keys.size == 1
|
243
|
+
|
244
|
+
# Iterate over keys to preserve order so we can unpack result
|
245
|
+
# like:
|
246
|
+
# foo, bar = product[:foo, :bar]
|
247
|
+
keys.map { |k| @meta[k] }
|
248
|
+
end
|
249
|
+
|
250
|
+
def update(values)
|
251
|
+
# Provide immutable update
|
252
|
+
Product.new(@src, @dst, @meta.merge(values))
|
253
|
+
end
|
254
|
+
|
255
|
+
def exist?
|
256
|
+
File.exist?(@src)
|
257
|
+
end
|
258
|
+
|
259
|
+
def inspect
|
260
|
+
'Product[src=\'%s\', dst=\'%s\', meta=%s]'.format(src, dst, meta)
|
261
|
+
end
|
262
|
+
|
263
|
+
def map
|
264
|
+
src, dst, meta = yield(@src, @dst, @meta)
|
265
|
+
Product.new(src || @src, dst || @dst, meta || @meta)
|
266
|
+
end
|
267
|
+
|
268
|
+
def to_s
|
269
|
+
inspect
|
270
|
+
end
|
271
|
+
|
272
|
+
def ==(other)
|
273
|
+
# Everything which not a Product, can't be equal to Product
|
274
|
+
return false unless other.is_a? Product
|
275
|
+
|
276
|
+
# Should we include meta in comparison?
|
277
|
+
@src.eql?(other.src) && @dst.eql?(other.dst)
|
278
|
+
end
|
279
|
+
|
280
|
+
def hash
|
281
|
+
@src.hash
|
282
|
+
end
|
283
|
+
|
284
|
+
def eql?(other)
|
285
|
+
self == other
|
286
|
+
end
|
287
|
+
|
288
|
+
def basename(*args)
|
289
|
+
File.basename(@dst, *args)
|
290
|
+
end
|
291
|
+
|
292
|
+
# Including helper classes
|
293
|
+
# Utility logic extracted from Product class to keep it clean and concise
|
294
|
+
class << self
|
295
|
+
def expand(expr, task = nil)
|
296
|
+
Grably::Core::ProductExpand.expand(expr, task)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rake/task'
|
2
|
+
|
3
|
+
require_relative 'task/bucket'
|
4
|
+
require_relative 'task/jobs'
|
5
|
+
require_relative 'task/expand'
|
6
|
+
require_relative 'task/enchancer'
|
7
|
+
|
8
|
+
module Grably
|
9
|
+
# All working files are stored under .grably directory.
|
10
|
+
WORKING_DIR = '.grably'.freeze
|
11
|
+
|
12
|
+
module Core
|
13
|
+
# We use Grably::Core::Task as alias to Rake::Task
|
14
|
+
Task = Rake::Task
|
15
|
+
|
16
|
+
# Here we will put extension methods for task
|
17
|
+
module TaskExtensions
|
18
|
+
include Grably::Core::TaskExtensions::Bucket
|
19
|
+
include Grably::Core::TaskExtensions::Jobs
|
20
|
+
include Grably::Core::TaskExtensions::Expand
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
module Rake
|
26
|
+
class Task # :nodoc:
|
27
|
+
include Grably::Core::TaskExtensions
|
28
|
+
include Grably::Core::TaskEnchancer
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Grably
|
2
|
+
module Core
|
3
|
+
module TaskExtensions
|
4
|
+
# # Bucket
|
5
|
+
# Bucket keeps result of task execution
|
6
|
+
module Bucket
|
7
|
+
# Updates bucket with result of argument expand
|
8
|
+
# @see [Grably::Core::ProductExpand]
|
9
|
+
# @param product_expr
|
10
|
+
# @return [Task]
|
11
|
+
def <<(product_expr)
|
12
|
+
expand = Product.expand(product_expr, self)
|
13
|
+
ensure_bucket
|
14
|
+
@bucket += expand
|
15
|
+
|
16
|
+
self # Allow chaining calls like
|
17
|
+
end
|
18
|
+
|
19
|
+
def bucket
|
20
|
+
ensure_bucket
|
21
|
+
end
|
22
|
+
|
23
|
+
def ensure_bucket
|
24
|
+
@bucket ||= []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Grably
|
2
|
+
module Core
|
3
|
+
# Wraps execute method, to print fancy info
|
4
|
+
# about task and its execution.
|
5
|
+
module TaskEnchancer
|
6
|
+
class << self
|
7
|
+
def included(other_class)
|
8
|
+
other_class.class_eval do
|
9
|
+
alias_method :old_execute, :execute
|
10
|
+
|
11
|
+
def execute(*args)
|
12
|
+
log_execute
|
13
|
+
FileUtils.mkdir_p(task_dir)
|
14
|
+
old_execute(*args)
|
15
|
+
export(to_s, Grably.export_path) if Grably.export_tasks.include?(to_s)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def export(name, export_path)
|
22
|
+
puts "Exporting task #{name}"
|
23
|
+
products = bucket
|
24
|
+
# Evacuating files. When task is finished other task execution
|
25
|
+
# may begin and all files that should be exported will be
|
26
|
+
# spoiled.
|
27
|
+
|
28
|
+
# Replace all ':' with '_'. When task name conatains ':' it
|
29
|
+
# means that task declared inside namespace. On Windows we can't
|
30
|
+
# create directory with ':' in name
|
31
|
+
dir_name = name.tr(':', '_')
|
32
|
+
dst = File.join(File.dirname(export_path), dir_name)
|
33
|
+
Grably.exports << cp_smart(products, dst)
|
34
|
+
end
|
35
|
+
|
36
|
+
def task_dir
|
37
|
+
File.join(WORKING_DIR, c.profile.join('-'), name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def log_execute
|
41
|
+
print "* #{self}".blue.bright
|
42
|
+
if Grably.export?
|
43
|
+
puts " (#{Dir.pwd}, #{c.profile.join('/').yellow})".white.bright
|
44
|
+
else
|
45
|
+
puts
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|