grably 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|