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.
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