grably 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +202 -0
  3. data/README.md +2 -0
  4. data/exe/grably +2 -0
  5. data/lib/ext/class.rb +28 -0
  6. data/lib/grably.rb +83 -0
  7. data/lib/grably/cli.rb +46 -0
  8. data/lib/grably/core.rb +15 -0
  9. data/lib/grably/core/app/enchancer.rb +36 -0
  10. data/lib/grably/core/application.rb +8 -0
  11. data/lib/grably/core/colors.rb +86 -0
  12. data/lib/grably/core/commands.rb +23 -0
  13. data/lib/grably/core/commands/cp.rb +103 -0
  14. data/lib/grably/core/commands/digest.rb +12 -0
  15. data/lib/grably/core/commands/log.rb +19 -0
  16. data/lib/grably/core/commands/run.rb +85 -0
  17. data/lib/grably/core/commands/serialize.rb +16 -0
  18. data/lib/grably/core/configuration.rb +39 -0
  19. data/lib/grably/core/configuration/pretty_print.rb +22 -0
  20. data/lib/grably/core/digest.rb +93 -0
  21. data/lib/grably/core/dsl.rb +15 -0
  22. data/lib/grably/core/essentials.rb +49 -0
  23. data/lib/grably/core/module.rb +64 -0
  24. data/lib/grably/core/product.rb +301 -0
  25. data/lib/grably/core/task.rb +30 -0
  26. data/lib/grably/core/task/bucket.rb +29 -0
  27. data/lib/grably/core/task/enchancer.rb +50 -0
  28. data/lib/grably/core/task/expand.rb +15 -0
  29. data/lib/grably/core/task/jobs.rb +58 -0
  30. data/lib/grably/job.rb +28 -0
  31. data/lib/grably/job/class.rb +93 -0
  32. data/lib/grably/job/exceptions.rb +0 -0
  33. data/lib/grably/job/instance.rb +159 -0
  34. data/lib/grably/job/manifest.rb +67 -0
  35. data/lib/grably/jobs.rb +4 -0
  36. data/lib/grably/jobs/sync.rb +91 -0
  37. data/lib/grably/jobs/text.rb +4 -0
  38. data/lib/grably/jobs/text/erb.rb +40 -0
  39. data/lib/grably/jobs/text/json.rb +12 -0
  40. data/lib/grably/jobs/text/text.rb +21 -0
  41. data/lib/grably/jobs/text/yaml.rb +12 -0
  42. data/lib/grably/jobs/unzip.rb +1 -0
  43. data/lib/grably/jobs/upload.rb +1 -0
  44. data/lib/grably/jobs/zip.rb +2 -0
  45. data/lib/grably/jobs/zip/unzip.rb +24 -0
  46. data/lib/grably/jobs/zip/zip.rb +46 -0
  47. data/lib/grably/runner.rb +31 -0
  48. data/lib/grably/server.rb +83 -0
  49. data/lib/grably/utils/pretty_printer.rb +63 -0
  50. data/lib/grably/version.rb +12 -0
  51. 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