z_build 1.1.0
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.
- data/lib/z_build.rb +765 -0
- metadata +62 -0
data/lib/z_build.rb
ADDED
@@ -0,0 +1,765 @@
|
|
1
|
+
require 'tempfile'
|
2
|
+
|
3
|
+
# @author Shaun Bruno <shaun.bruno@gmail.com>
|
4
|
+
# @copyright Copyright 2013 Shaun Bruno
|
5
|
+
#
|
6
|
+
# Z'Build module is a collection of commonly used functions for creating
|
7
|
+
# light-weight build processes, written in Ruby. This library was authored in my spare time
|
8
|
+
# for {https://zoosk.com Zoosk} with a desire for more easily maintainable build processes,
|
9
|
+
# with a love for Ruby, and with a strong dislike for PHING/ANT and XML builds systems.
|
10
|
+
# At it's core the Z'Build suite is intended to be minimal, sufficient, and transparent.
|
11
|
+
# If there is some aspect of your existing build that cannot be translated or mimicked
|
12
|
+
# with a series of Z'Build functions, then there may well be a missing feature, or
|
13
|
+
# otherwise there might be an opportunity to simplify your processes. However, it is
|
14
|
+
# expected that custom needs will require specialized code and as a library Z'Build is
|
15
|
+
# designed to be easily extended and works very well in conjuncation with build tools
|
16
|
+
# such as Ruby Rake. It is not meant to be a stand-alone build replacement - Rake/Thor/etc
|
17
|
+
# already do that very well. Here's to hoping Z'Build makes your builds faster,
|
18
|
+
# your code simpler, and your life easier!
|
19
|
+
#
|
20
|
+
# @example
|
21
|
+
# # This simple example build requires a repository checkout and
|
22
|
+
# # a file system copy with file token replacement.
|
23
|
+
#
|
24
|
+
# require 'z_build'
|
25
|
+
# include ZBuild # brings module functions into global scope
|
26
|
+
#
|
27
|
+
# clock 'Starting Build' do
|
28
|
+
# set_deploy_dir '/path/to/release'
|
29
|
+
# run 'git clone https://github.com/your_organization/project.git'
|
30
|
+
# deploy_cp 'project/', '', :recurse => true, :tokens => props_to_hash('templates/props.txt')
|
31
|
+
# end
|
32
|
+
module ZBuild
|
33
|
+
|
34
|
+
# Run shell commands
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# run 'mysql -h127.0.0.1 -uroot -psomethingsupersecret', :regex_mask => /-p[a-z]*/, :mask_replace => '-p<BET YOU WISH YOU KNEW>'
|
38
|
+
# => [INFO] Running Command: mysql -h127.0.0.1 -uroot -p<BET YOU WISH YOU KNEW>
|
39
|
+
#
|
40
|
+
# @param [String] shell_command
|
41
|
+
# @param [Hash] opts options used for execution
|
42
|
+
# @option opts [String] :desc ('Running Command') description of command being run
|
43
|
+
# @option opts [bool] :quiet (false) true to supress description output of command
|
44
|
+
# @option opts [Regexp|String] :regex_mask when :quiet => false, mask output description
|
45
|
+
# of the characters captured by this regex or string
|
46
|
+
# @option opts [String] :mask_replace ('xxxx') mask used when :regex_mask => true
|
47
|
+
# @option opts [bool] :check_return (false) true to raise error when result of
|
48
|
+
# shell command ran is non-empty
|
49
|
+
# @return [String] resultant output from running shell command
|
50
|
+
# @raise [RuntimeError] when :check_return => true and shell command exec has a non empty result
|
51
|
+
def run(shell_command, opts={})
|
52
|
+
desc = opts[:desc] ? opts[:desc].to_s : "Running Command"
|
53
|
+
if !opts[:quiet]
|
54
|
+
output = "#{desc}: #{shell_command}"
|
55
|
+
|
56
|
+
if opts[:regex_mask]
|
57
|
+
mask_replace = opts[:mask_replace] ? opts[:mask_replace] : 'xxxx'
|
58
|
+
output.gsub! opts[:regex_mask], mask_replace
|
59
|
+
end
|
60
|
+
|
61
|
+
self.info output
|
62
|
+
end
|
63
|
+
|
64
|
+
result = `#{shell_command}`
|
65
|
+
|
66
|
+
if opts[:check_return] && !result.empty?
|
67
|
+
raise "run failed for shell command: #{shell_command}"
|
68
|
+
end
|
69
|
+
|
70
|
+
result
|
71
|
+
end
|
72
|
+
|
73
|
+
# Writes the content string to the specified file path.
|
74
|
+
#
|
75
|
+
# @param [String] path file path to write content string to relative to the set deploy path
|
76
|
+
# @param [String] str content string to write
|
77
|
+
# @param [Fixnum] perm the permissions for the file created
|
78
|
+
def deploy_write(path, str, perm=0750)
|
79
|
+
deploy_path = self.deploy_path path
|
80
|
+
|
81
|
+
File.open(deploy_path, 'w', perm) do |f|
|
82
|
+
f.write str
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Create all directories at the specified deploy path.
|
87
|
+
#
|
88
|
+
# @param [String] path directory path to create relative to the set deploy path
|
89
|
+
# @param [Fixnum] perm the permissions for the directory/directories created
|
90
|
+
# @note this function works like the shell mkdir -p, so that /multiple/paths/deep will create all
|
91
|
+
# directories in the path
|
92
|
+
def deploy_mkdir(path, perm=0750)
|
93
|
+
deploy_path = self.deploy_path path
|
94
|
+
|
95
|
+
# only create when does not already exist
|
96
|
+
if !File.exist? deploy_path
|
97
|
+
FileUtils.mkdir_p deploy_path, :mode => perm
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Copy files from the working directory to deploy path. When the recurse flag is set,
|
102
|
+
# contents from first directory's contents are always copied as content to the second directory.
|
103
|
+
# If the intended behavior is to copy the first directory itself (in unix shell cp -r
|
104
|
+
# /path/dir dest/ vs cp -r /path/dir/ dest/), then one should instead call
|
105
|
+
# deploy_mkdir prior to a deploy_copy. This simplistic behavior is to make the calls
|
106
|
+
# more explicit and consequences less subtle than in a standard shell. The best way
|
107
|
+
# to conceptualzie this behavior is <b>deploy_cp always copies directory content,
|
108
|
+
# and only sub-directories (as content) when the recurse flag is set</b>.
|
109
|
+
#
|
110
|
+
# @example
|
111
|
+
# # this call required once for all subsequent calls
|
112
|
+
# set_deploy_dir '/Users/myname/Dropbox'
|
113
|
+
#
|
114
|
+
# # will copy file to /Users/myname/Dropbox/text_file.txt
|
115
|
+
# deploy_cp '/Users/myname/Documents/text_file.txt', ''
|
116
|
+
#
|
117
|
+
# # will copy file and rename to /Users/myname/Dropbox/new_name.txt
|
118
|
+
# deploy_cp '/Users/myname/Documents/text_file.txt', 'new_name.txt'
|
119
|
+
#
|
120
|
+
# # prior example with token replacement
|
121
|
+
# deploy_cp '/Users/myname/Documents/text_file.txt', 'new_name.txt', :tokens => {:DOG_TYPE => 'Chihuahua'}
|
122
|
+
#
|
123
|
+
# # will copy all txt files to /Users/myname/Dropbox/
|
124
|
+
# deploy_cp '/Users/myname/Documents/*.txt', './'
|
125
|
+
#
|
126
|
+
# # will copy all txt files to /Users/myname/Dropbox/, excluding text_file.txt
|
127
|
+
# deploy_cp '/Users/myname/Documents/*.txt', './', :exclude => '/Users/myname/Documents/text_file.txt'
|
128
|
+
#
|
129
|
+
# # working directory set allows for less vebose copying
|
130
|
+
# set_working_dir '/Users/myname/Documents/'
|
131
|
+
# deploy_cp '*.txt', './', :exclude => 'text_file.txt'
|
132
|
+
#
|
133
|
+
# # working directory set not required for use
|
134
|
+
# reset_working_dir
|
135
|
+
# # will recursively copy all files and directories within Documents/ to /Users/myname/Dropbox/
|
136
|
+
# deploy_cp '/Users/myname/Documents/', './', :recurse => true
|
137
|
+
#
|
138
|
+
# # prior example with token replacement on every file copied
|
139
|
+
# deploy_cp '/Users/myname/Documents/', './', :recurse => true, :tokens => {:DOG_TYPE => 'Chihuahua'}
|
140
|
+
#
|
141
|
+
# @param [String] from path to copy from working directory, may be a glob when :recurse => false,
|
142
|
+
# must be a directory when :recurse => true
|
143
|
+
# @param [String] to destination path to copy file/files, may be a file when :recurse => false,
|
144
|
+
# must be a directory when :recurse => true
|
145
|
+
# @param [Hash] opts options for copy operation
|
146
|
+
# @option opts [String] :recurse (FalseClass) recursively copy from to deploy path
|
147
|
+
# retaining all original file and directory names
|
148
|
+
# @option opts [Array] :exclude list of files and directories to exclude from copying,
|
149
|
+
# specified by paths relative to the currently set working directory
|
150
|
+
# @option opts [Hash] :tokens has with tokens keyed to their replacements to use on every file copied
|
151
|
+
# @option opts [String] :token_prefix when :tokens are set consider prefix for every replacement
|
152
|
+
# @option opts [String] :token_suffix when :tokens are set consider suffix for every replacement
|
153
|
+
#
|
154
|
+
# @raise [RuntimeError] on attempting to copy a directory as [from] when :recurse => false
|
155
|
+
# @raise [RuntimeError] when :recurse => true and [from] and [to] paths are not both directories
|
156
|
+
#
|
157
|
+
# @see #set_working_dir
|
158
|
+
# @see #set_deploy_dir
|
159
|
+
#
|
160
|
+
def deploy_cp(from, to, opts={})
|
161
|
+
working_path_given = self.working_path from
|
162
|
+
deploy_path_given = self.deploy_path to
|
163
|
+
queued_dirs = []
|
164
|
+
queued_files = {}
|
165
|
+
working_exclude = opts[:exclude] ? opts[:exclude].to_a : []
|
166
|
+
|
167
|
+
if opts[:recurse]
|
168
|
+
|
169
|
+
if !File.directory? from
|
170
|
+
raise "Working path must be a directory when :recurse => true, given: #{working_path_given}"
|
171
|
+
end
|
172
|
+
|
173
|
+
if !File.directory? deploy_path_given
|
174
|
+
raise "Deploy path must be a directory when :recurse => true, given: #{deploy_path_given}"
|
175
|
+
end
|
176
|
+
|
177
|
+
if !opts[:tokens] && !opts[:exclude]
|
178
|
+
# when no token replacement and no exclusion is required, faster to do system copy
|
179
|
+
# always join path to /. so that the contents will always be copied, and never the
|
180
|
+
# directory itself - this maintains consistent behavior for the function
|
181
|
+
# FileUtils requires the slash-dot /. whereas shell only requires either / or /*
|
182
|
+
FileUtils.cp_r File.join(working_path_given, '/').concat('.'), deploy_path_given
|
183
|
+
return
|
184
|
+
end
|
185
|
+
|
186
|
+
# from - the directory relative to working dir
|
187
|
+
# deploy_path_given - the directory relative to the deploy dir
|
188
|
+
|
189
|
+
self.walk_path(from) do |f_path, is_dir|
|
190
|
+
if working_exclude.include? f_path.sub('./','') # glob prefixes ./ relative to root
|
191
|
+
# ignore files or directories marked for exclusion
|
192
|
+
next
|
193
|
+
end
|
194
|
+
|
195
|
+
if is_dir
|
196
|
+
# deploy_mkdir already creates relative to deploy path, so enqueue relative path
|
197
|
+
queued_dirs.push File.join to, f_path.sub(from, '')
|
198
|
+
next
|
199
|
+
end
|
200
|
+
|
201
|
+
# when recurse is invoked always copy the actual file name to the deploy dir given
|
202
|
+
queued_files[f_path] = File.join deploy_path_given, f_path.sub(from, '')
|
203
|
+
end
|
204
|
+
else
|
205
|
+
if File.directory? self.working_path(from)
|
206
|
+
raise "Path given is a directory - not copied: #{self.working_path(from)}"
|
207
|
+
end
|
208
|
+
|
209
|
+
if !opts[:tokens] && !opts[:exclude]
|
210
|
+
# when no token replacement and no exclusion is required, faster to do system copy
|
211
|
+
if working_path_given.include? '*'
|
212
|
+
FileUtils.cp Dir.glob(working_path_given), deploy_path_given
|
213
|
+
else
|
214
|
+
FileUtils.cp working_path_given, deploy_path_given
|
215
|
+
end
|
216
|
+
return
|
217
|
+
end
|
218
|
+
|
219
|
+
self.glob(from) do |f_path, is_dir|
|
220
|
+
if working_exclude.include? f_path.sub('./','') # glob prefixes ./ relative to root
|
221
|
+
# ignore files or directories marked for exclusion
|
222
|
+
next
|
223
|
+
end
|
224
|
+
|
225
|
+
if File.directory? deploy_path_given
|
226
|
+
# given dir, so retain name of original file
|
227
|
+
dest_path = File.join(deploy_path_given, File.basename(f_path))
|
228
|
+
else
|
229
|
+
# deploy_path given is an actual file target
|
230
|
+
dest_path = deploy_path_given
|
231
|
+
end
|
232
|
+
|
233
|
+
# f_path provided is relative to working dir - dest is absolute to deploy target
|
234
|
+
queued_files[f_path] = dest_path
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# all directories queued are relative to deploy target
|
239
|
+
queued_dirs.each do |d|
|
240
|
+
self.deploy_mkdir d
|
241
|
+
end
|
242
|
+
|
243
|
+
queued_files.each do |relative_src, absolute_dest|
|
244
|
+
File.open(absolute_dest, 'w') do |fh|
|
245
|
+
if opts[:tokens].is_a? Hash
|
246
|
+
fh.write self.token_replace_file(relative_src, opts[:tokens], opts[:token_prefix].to_s, opts[:token_suffix].to_s)
|
247
|
+
else
|
248
|
+
fh.write self.file_to_str(relative_src)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Specify a deploy directory - with a deploy directory set all functions requiring
|
255
|
+
# absolute paths for deploy parameters will become relative to the deploy directory set.
|
256
|
+
# This is not to be confused with the working directory - (see set_working_dir)
|
257
|
+
#
|
258
|
+
# @param [String] path the directory to set as the deploy directory.
|
259
|
+
# @return [String] the path set
|
260
|
+
# @raise [RuntimeError] when specified path is not a valid directory
|
261
|
+
# @see #set_working_dir
|
262
|
+
#
|
263
|
+
# @note When deploy dir is set all function paramaters for the deploy directory will
|
264
|
+
# be assumed relative as well.
|
265
|
+
#
|
266
|
+
def set_deploy_dir(path)
|
267
|
+
if !File.directory? path
|
268
|
+
raise "Specified path is not a directory: #{path}"
|
269
|
+
end
|
270
|
+
|
271
|
+
@deploy_dir = path
|
272
|
+
end
|
273
|
+
|
274
|
+
# Clears the deploy directory variable - requiring ZBuild specified deploy paths to be absolute.
|
275
|
+
# By default all specified deploy paths must be absolute, however when a deploy
|
276
|
+
# directory is set, invoking this method requires them to be absolute again.
|
277
|
+
#
|
278
|
+
# @see #set_deploy_dir
|
279
|
+
#
|
280
|
+
def reset_deploy_dir
|
281
|
+
@deploy_dir = nil
|
282
|
+
end
|
283
|
+
|
284
|
+
# Specify a working directory - with a working directory set all functions requiring
|
285
|
+
# absolute paths will become relative to the working directory. The exception is
|
286
|
+
# paths meant for deploy function parameters - (see set_deploy_dir)
|
287
|
+
#
|
288
|
+
# @example
|
289
|
+
# str1 = file_to_str '/absolute/path/to/file.txt'
|
290
|
+
#
|
291
|
+
# set_working_dir '/absolute'
|
292
|
+
# str2 = file_to_str 'path/to/file.txt'
|
293
|
+
#
|
294
|
+
# str1 == str2
|
295
|
+
# => true
|
296
|
+
#
|
297
|
+
# @param [String] path the directory to set as the working directory.
|
298
|
+
# @return [String] the path set
|
299
|
+
# @raise [RuntimeError] when specified path is not a valid directory
|
300
|
+
# @see #set_deploy_dir
|
301
|
+
# @note When working dir is set all paths yielded by blocks will be relative as well.
|
302
|
+
# @note When working dir is set all function paramaters for the working directory will
|
303
|
+
# be assumed relative as well.
|
304
|
+
#
|
305
|
+
def set_working_dir(path)
|
306
|
+
if !File.directory? path
|
307
|
+
raise "Specified path is not a directory: #{path}"
|
308
|
+
end
|
309
|
+
|
310
|
+
@working_dir = path
|
311
|
+
end
|
312
|
+
|
313
|
+
# Clears the working directory variable - requiring ZBuild specified paths to be absolute.
|
314
|
+
# By default all specified paths must be absolute, however when a working directory is
|
315
|
+
# set, invoking this method requires them to be absolute again.
|
316
|
+
#
|
317
|
+
# @see #set_working_dir
|
318
|
+
#
|
319
|
+
def reset_working_dir
|
320
|
+
@working_dir = nil
|
321
|
+
end
|
322
|
+
|
323
|
+
# Retrieve a path relative to the set working directory.
|
324
|
+
#
|
325
|
+
# @example
|
326
|
+
# working_path 'user_name/logs'
|
327
|
+
# => user_name/logs
|
328
|
+
#
|
329
|
+
# set_working_path '/tmp/user_name'
|
330
|
+
# working_path 'logs'
|
331
|
+
# => /tmp/user_name/logs
|
332
|
+
#
|
333
|
+
# @param [String] path file path
|
334
|
+
# @return [String] the path specified, relative to the set working dir
|
335
|
+
# @see #set_working_dir
|
336
|
+
#
|
337
|
+
def working_path(path)
|
338
|
+
working_dir = self.working_dir
|
339
|
+
working_dir.empty? ? path : File.join(working_dir, path)
|
340
|
+
end
|
341
|
+
|
342
|
+
# Retrieve a path relative to the set deploy directory.
|
343
|
+
#
|
344
|
+
# @param [String] path file path
|
345
|
+
# @return [String] the path specified, relative to the set working dir
|
346
|
+
# @raise [RuntimeError] when deploy path has not been set
|
347
|
+
# @see #working_path
|
348
|
+
def deploy_path(path)
|
349
|
+
deploy_dir = self.deploy_dir
|
350
|
+
if deploy_dir.empty?
|
351
|
+
raise 'Deploy directory not set - set_deploy_dir must be invoked before deploy function use'
|
352
|
+
end
|
353
|
+
|
354
|
+
File.join(deploy_dir, path)
|
355
|
+
end
|
356
|
+
|
357
|
+
# @return [String] the set working directory, or empty string if not set
|
358
|
+
# @see #set_working_dir
|
359
|
+
def working_dir
|
360
|
+
@working_dir ||= ''
|
361
|
+
end
|
362
|
+
|
363
|
+
# @return [String] the set deploy directory, or empty string if not set
|
364
|
+
# @see #set_deploy_dir
|
365
|
+
def deploy_dir
|
366
|
+
@deploy_dir ||= ''
|
367
|
+
end
|
368
|
+
|
369
|
+
# Perform token replacement on the specified string, with optionally specified token
|
370
|
+
# prefix and suffixes.
|
371
|
+
#
|
372
|
+
# @example
|
373
|
+
# str = "##HELLO##, World"
|
374
|
+
# hash = {:HELLO => 'Greetings'}
|
375
|
+
#
|
376
|
+
# token_replace_str(str, hash, '##', '##)
|
377
|
+
# => Greetings, World
|
378
|
+
#
|
379
|
+
# @param [String] str the string to perform token replacmeent on
|
380
|
+
# @param [Hash] token_to_replacement_hash hash keyed by tokens mapped to values for replacement
|
381
|
+
# @param [String] token_prefix prefix present on every token within str, but omitted from
|
382
|
+
# token_to_replacement hash keys
|
383
|
+
# @param [String] token_suffix suffix present to every token within str, but omitted from
|
384
|
+
# token_to_replacement hash keys
|
385
|
+
# @return [String] the given str with tokens replaced by their respectively mapped values
|
386
|
+
#
|
387
|
+
def token_replace_str(str, token_to_replacement_hash, token_prefix='', token_suffix='')
|
388
|
+
token_to_replacement_hash.each do |token, replacement|
|
389
|
+
str = str.gsub "#{token_prefix}#{token}#{token_suffix}", replacement.to_s
|
390
|
+
end
|
391
|
+
|
392
|
+
str
|
393
|
+
end
|
394
|
+
|
395
|
+
# Shortcut function to combine file_to_str + token_replace_str.
|
396
|
+
#
|
397
|
+
# @param [String] file the file to read and perform token replacmeent on
|
398
|
+
# @param [Hash] token_to_replacement_hash hash keyed by tokens mapped to values for replacement
|
399
|
+
# @param [String] token_prefix prefix present on every token within str, but omitted from
|
400
|
+
# token_to_replacement hash keys
|
401
|
+
# @param [String] token_suffix suffix present to every token within str, but omitted from
|
402
|
+
# token_to_replacement hash keys
|
403
|
+
# @return [String] the given str with tokens replaced by their respectively mapped values
|
404
|
+
#
|
405
|
+
# @see #file_to_str
|
406
|
+
# @see #token_replace_str
|
407
|
+
#
|
408
|
+
def token_replace_file(file, token_to_replacement_hash, token_prefix='', token_suffix='')
|
409
|
+
# file_to_str accepts the path relative to working dir
|
410
|
+
str = file_to_str file
|
411
|
+
|
412
|
+
token_to_replacement_hash.each do |token, replacement|
|
413
|
+
str = str.gsub "#{token_prefix}#{token}#{token_suffix}", replacement.to_s
|
414
|
+
end
|
415
|
+
|
416
|
+
str
|
417
|
+
end
|
418
|
+
|
419
|
+
# Reads the specified file and returns contents as a string.
|
420
|
+
#
|
421
|
+
# @example
|
422
|
+
# str = file_to_str '/path/to/your/file.txt'
|
423
|
+
#
|
424
|
+
# @param [String] file file to read into a string
|
425
|
+
# @return [String] string content of the file
|
426
|
+
# @raise [RuntimeError] when specified file does not exist
|
427
|
+
#
|
428
|
+
def file_to_str(file)
|
429
|
+
str = ''
|
430
|
+
|
431
|
+
working_path = File.join self.working_dir, file
|
432
|
+
|
433
|
+
if !File.file? working_path
|
434
|
+
raise "Specified file does not exist: #{working_path}"
|
435
|
+
end
|
436
|
+
|
437
|
+
File.open(working_path) do |fh|
|
438
|
+
fh.each do |line|
|
439
|
+
str += line
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
str
|
444
|
+
end
|
445
|
+
|
446
|
+
# Given a specified path glob, yields the files and directories
|
447
|
+
# matching the glob pattern.
|
448
|
+
#
|
449
|
+
# @param [String] pattern file glob pattern
|
450
|
+
# @yield [file_path, is_dir] path of the file and bool as to whether it is a directory
|
451
|
+
#
|
452
|
+
def glob(pattern)
|
453
|
+
relative_dir = File.dirname(pattern)
|
454
|
+
working_dir = self.working_path pattern
|
455
|
+
|
456
|
+
Dir.glob working_dir do |file_path|
|
457
|
+
file_name = File.basename file_path
|
458
|
+
is_dir = File.directory?(file_path)
|
459
|
+
|
460
|
+
yield File.join(relative_dir, file_name), is_dir
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# Walk to maxiumum depth the directory structure provided.
|
465
|
+
#
|
466
|
+
# @example
|
467
|
+
# walk_path '/mount/dev' do |path, is_dir|
|
468
|
+
# puts "#{is_dir ? 'Dir' : 'File'} #{path}"
|
469
|
+
# end
|
470
|
+
# => ........
|
471
|
+
# => File /mount/dev/branch/docs/doc/top-level-namespace.html
|
472
|
+
# => File /mount/dev/branch/docs/doc/ZDatabase.html
|
473
|
+
# => Dir /mount/dev/branch/docs/doc
|
474
|
+
# => Dir /mount/dev/branch/docs
|
475
|
+
# => ........
|
476
|
+
#
|
477
|
+
# @param [String] path the directory path to traverse, if a file is provided the file's
|
478
|
+
# directory will be used instead
|
479
|
+
# @yield [file_path, is_dir] path of the file and bool as to whether it is a directory
|
480
|
+
#
|
481
|
+
def walk_path(path, &block)
|
482
|
+
self.glob(File.join(path, '*')) do |f, is_dir|
|
483
|
+
|
484
|
+
if is_dir
|
485
|
+
self.walk_path f, &block
|
486
|
+
end
|
487
|
+
|
488
|
+
yield f, is_dir
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# Clock the time required to perform the operations within the given
|
493
|
+
# block. This function will also keep track of the number of nested
|
494
|
+
# clocks, and for easy-reading indent accordingly.
|
495
|
+
#
|
496
|
+
# @example
|
497
|
+
# clock "Performing some super cool operation" do
|
498
|
+
# info "Like just printing a message and going to sleep."
|
499
|
+
# sleep(1)
|
500
|
+
# end
|
501
|
+
#
|
502
|
+
# => Performing some super cool operation
|
503
|
+
# => [INFO] Like just printing a message and going to sleep.
|
504
|
+
# => Done [1 sec]
|
505
|
+
# => 1
|
506
|
+
#
|
507
|
+
# @param [String] desc the description of the operation being clocked
|
508
|
+
# @yield to provided block
|
509
|
+
# @return [Fixnum] the number of seconds recorded for the clocked operation
|
510
|
+
#
|
511
|
+
def clock(desc='Clocking Operation', &block)
|
512
|
+
@clock_count ||=0
|
513
|
+
@clock_count += 1
|
514
|
+
|
515
|
+
indent = " " * [0,@clock_count - 1].max
|
516
|
+
|
517
|
+
puts "#{indent}#{desc}"
|
518
|
+
t = Time.now.to_i
|
519
|
+
yield
|
520
|
+
run_time_sec = Time.now.to_i - t
|
521
|
+
|
522
|
+
if run_time_sec < 60
|
523
|
+
run_time_str = "#{run_time_sec} sec"
|
524
|
+
elsif run_time_sec < 3600
|
525
|
+
run_time_str = "#{run_time_sec / 60} min #{run_time_sec % 60} sec"
|
526
|
+
else
|
527
|
+
run_time_str = "#{run_time_sec / 3600} hr #{run_time_sec % 3600 / 60} min #{run_time_sec % 60} sec"
|
528
|
+
end
|
529
|
+
|
530
|
+
puts "#{indent}Done [#{run_time_str}]\n"
|
531
|
+
|
532
|
+
@clock_count -= 1
|
533
|
+
|
534
|
+
run_time_sec
|
535
|
+
end
|
536
|
+
|
537
|
+
# Short form of open_temp_file - writes content string immediately to temp file
|
538
|
+
# and yields the generated path. The temp file will be automatically deleted
|
539
|
+
# from the file system at the completion of the block.
|
540
|
+
#
|
541
|
+
# @example
|
542
|
+
# temp_file sql_content_str, 'Sql file' do |path|
|
543
|
+
# `myqsl < #{path}`
|
544
|
+
# end
|
545
|
+
# => /var/folders/path/to/temp/z_build_sql_file_136487088920130401-7393-18ynao0-0
|
546
|
+
#
|
547
|
+
# @param [String] content to write to the temporary file
|
548
|
+
# @param [String] desc description of the temporary file created or purpose for creation
|
549
|
+
# @yield [f_path] temporary file's associated path
|
550
|
+
# @note on yield the file has already been closed and the path is ready for reading/use
|
551
|
+
# @return [String] fully qualified path of the file created (and destroyed following the block)
|
552
|
+
# @see #open_temp_file
|
553
|
+
#
|
554
|
+
def temp_file(content, desc='', &block)
|
555
|
+
f_path = ''
|
556
|
+
|
557
|
+
self.open_temp_file do |f_handle, path|
|
558
|
+
f_path = path
|
559
|
+
f_handle.write content
|
560
|
+
f_handle.close
|
561
|
+
yield path
|
562
|
+
end
|
563
|
+
|
564
|
+
f_path
|
565
|
+
end
|
566
|
+
|
567
|
+
# Creates a temporary file and returns generated path and file handle to the provided
|
568
|
+
# block. The file will be automatically deleted from the file system at the
|
569
|
+
# completion of the block. This function is useful in distinction from self.temp_file
|
570
|
+
# in that the temp file may be written to incrementally, rather than at once with a
|
571
|
+
# single string parameter, which under circumstance could cause a memory buffer error.
|
572
|
+
#
|
573
|
+
# @example
|
574
|
+
# open_temp_file 'Sql file' do |f_handle, path|
|
575
|
+
# f.write my_sql_string_defined_previously
|
576
|
+
#
|
577
|
+
# # must close before the file may be read from the path
|
578
|
+
# f.close
|
579
|
+
#
|
580
|
+
# `myqsl < #{path}`
|
581
|
+
# end
|
582
|
+
# => /var/folders/path/to/temp/z_build_sql_file_136487088920130401-7393-18ynao0-0
|
583
|
+
#
|
584
|
+
# @param [String] desc description of the temporary file created or purpose for creation
|
585
|
+
# @yield [f_handle, f_path] temporary file's handle and associated path
|
586
|
+
# @return [String] fully qualified path of the file created (and destroyed following the block)
|
587
|
+
# @see #temp_file
|
588
|
+
#
|
589
|
+
def open_temp_file(desc='', &block)
|
590
|
+
desc = desc.empty? ? '' : desc.to_s.match(/[a-zA-Z\s\d]*/).to_s.downcase.gsub(' ', '_')
|
591
|
+
|
592
|
+
f_handle = Tempfile.new ['z_build', desc, Time.now.to_i].join('_')
|
593
|
+
f_path = f_handle.path
|
594
|
+
begin
|
595
|
+
yield f_handle, f_handle.path
|
596
|
+
|
597
|
+
if !f_handle.closed?
|
598
|
+
f_handle.close
|
599
|
+
end
|
600
|
+
ensure
|
601
|
+
f_handle.unlink
|
602
|
+
end
|
603
|
+
|
604
|
+
f_path
|
605
|
+
end
|
606
|
+
|
607
|
+
# Sends to stdout the warning message provided.
|
608
|
+
# Will indent relative to the number of clocked operations.
|
609
|
+
#
|
610
|
+
# @example
|
611
|
+
# clock 'Usage Example' do
|
612
|
+
# warn 'Testing Warning'
|
613
|
+
# end
|
614
|
+
#
|
615
|
+
# => Usage Example
|
616
|
+
# => [WARN] Testing Warning
|
617
|
+
# => Done [0 sec]
|
618
|
+
#
|
619
|
+
# @param [String] message
|
620
|
+
def warn(message)
|
621
|
+
puts "#{self.clock_indent}[WARN] #{message}"
|
622
|
+
end
|
623
|
+
|
624
|
+
# Sends to stdout the message provided.
|
625
|
+
# Will indent relative to the number of clocked operations.
|
626
|
+
#
|
627
|
+
# @param [String] message
|
628
|
+
def info(message)
|
629
|
+
puts "#{self.clock_indent}[INFO] #{message}"
|
630
|
+
end
|
631
|
+
|
632
|
+
# @todo add a no_merge option
|
633
|
+
#
|
634
|
+
# Converts all KEY=VALUE pairs in the specified file to a hash and perform $\{var}
|
635
|
+
# replacement.
|
636
|
+
# Lines starting with ruby comments '#' will be ignored.
|
637
|
+
# Comments trailing a KEY=VALUE pair will be stripped.
|
638
|
+
#
|
639
|
+
# Variables must be surrounded by a dollars with parenthesis e.g. $\{var} and
|
640
|
+
# the variable must be alpha or alpha-numeric and may contain dashes or underscores.
|
641
|
+
#
|
642
|
+
# Variables may also be defined NOT using TOP-DOWN ordering, <b>though this is strongly
|
643
|
+
# discouraged.</b> See below an example of top-down and bottom-up
|
644
|
+
# ordering working correctly:
|
645
|
+
#
|
646
|
+
# @example
|
647
|
+
# == /path/to/example.txt
|
648
|
+
#
|
649
|
+
# # comment lines will be ignored
|
650
|
+
# NAME=Bruno
|
651
|
+
# GREETING=Hello, ${NAME}. # side comments will also be discarded
|
652
|
+
#
|
653
|
+
# HERO=super${SPECIES}
|
654
|
+
# SPECIES=man
|
655
|
+
#
|
656
|
+
# == irb
|
657
|
+
#
|
658
|
+
# require 'z_build'
|
659
|
+
#
|
660
|
+
# props_to_hash '/path/to/example.txt'
|
661
|
+
# => {"SPECIES"=>"man", "GREETING"=>"Hello, Bruno.", "HERO"=>"superman", "NAME"=>"Bruno"}
|
662
|
+
#
|
663
|
+
# @param [String] file fully qualified file path containing the file to read
|
664
|
+
# @param [Hash] hash hash to combine with - keys read from file will overwrite keys
|
665
|
+
# found in this hash, and the combined hash will be used to resolve
|
666
|
+
# variables specified in the dollar-sign block syntax $\{var} in the value field.
|
667
|
+
# Only values may be variable replaced; keys in the file will not be variable replaced.
|
668
|
+
# For efficiency the hash parameter will not be copied, therefore if the desired
|
669
|
+
# behavior is to not overwrite the hash given (during combination with file
|
670
|
+
# key/values), hash.clone should be passed as the function parameter.
|
671
|
+
#
|
672
|
+
# @return [Hash] containing key/value pairs found in the specified file path,
|
673
|
+
# combined with the hash parameter
|
674
|
+
#
|
675
|
+
# @raise [RuntimeError] when specified file does not exist
|
676
|
+
#
|
677
|
+
def props_to_hash(file, hash={})
|
678
|
+
file_path = self.working_path file
|
679
|
+
reprocess_keys = []
|
680
|
+
var_regex = /\$\{([a-zA-Z|\-|_|\d]+)\}/
|
681
|
+
|
682
|
+
if !File.file? file_path
|
683
|
+
raise "Specified file does not exist: #{file_path}"
|
684
|
+
end
|
685
|
+
|
686
|
+
File.open(file_path) do |f|
|
687
|
+
f.each do |line|
|
688
|
+
line.to_s.strip!
|
689
|
+
|
690
|
+
if line.empty? then next; end
|
691
|
+
if line.start_with? '#' then next; end
|
692
|
+
|
693
|
+
key, value = line.split '='
|
694
|
+
key = key.to_s.strip.to_s
|
695
|
+
value = value.to_s.gsub(/#.*/, '').strip.to_s
|
696
|
+
|
697
|
+
hash[key] = value
|
698
|
+
|
699
|
+
# replace variable values defined by prior k,v pairs
|
700
|
+
value.scan(var_regex).flatten.each do |var|
|
701
|
+
if hash[var] == nil
|
702
|
+
# key is expected but is not yet defined
|
703
|
+
# mark key for re-visitation post processing
|
704
|
+
reprocess_keys.push key
|
705
|
+
next
|
706
|
+
end
|
707
|
+
|
708
|
+
# variable was found in hash - update keyed value with var replacement
|
709
|
+
hash[key] = value.gsub("${#{var}}", hash[var])
|
710
|
+
end
|
711
|
+
end
|
712
|
+
end
|
713
|
+
|
714
|
+
# final pass var replacement for variables not defined using TOP-DOWN ordering
|
715
|
+
reprocess_keys.each do |key|
|
716
|
+
hash[key].scan(var_regex).flatten.each do |var|
|
717
|
+
if hash[var] == nil
|
718
|
+
# key is expected but is not yet defined - will not reprocess this variable
|
719
|
+
self.warn "Failed to replace variable[#{var}] mapped to key[#{key}] found in file[#{file_path}]"
|
720
|
+
next
|
721
|
+
end
|
722
|
+
|
723
|
+
hash[key] = hash[key].gsub("${#{var}}", hash[var])
|
724
|
+
end
|
725
|
+
end
|
726
|
+
|
727
|
+
hash
|
728
|
+
end
|
729
|
+
|
730
|
+
# N-ary alternative to props_to_hash. Takes n-property files, merges their key/value
|
731
|
+
# pairs into a hash in FIFO ordering, while performing variable replacement
|
732
|
+
#
|
733
|
+
# @example
|
734
|
+
# hash = props_list_to_hash file1, file2, file3, file4
|
735
|
+
#
|
736
|
+
# @param [Array] files list of files to read key/value pairs into a combined hash
|
737
|
+
# @raise [RuntimeError] when one of the specified files does not exist
|
738
|
+
# @see #props_to_hash
|
739
|
+
#
|
740
|
+
def props_list_to_hash(*files)
|
741
|
+
h = {}
|
742
|
+
files.each do |f|
|
743
|
+
h = self.props_to_hash f, h
|
744
|
+
end
|
745
|
+
|
746
|
+
h
|
747
|
+
end
|
748
|
+
|
749
|
+
protected
|
750
|
+
|
751
|
+
# Module method
|
752
|
+
def self.included(base)
|
753
|
+
# also add these methods to the class object including ZBuild
|
754
|
+
base.extend self
|
755
|
+
end
|
756
|
+
|
757
|
+
def clock_count
|
758
|
+
@clock_count ? @clock_count : 0
|
759
|
+
end
|
760
|
+
|
761
|
+
def clock_indent
|
762
|
+
# use soft tabs
|
763
|
+
" " * self.clock_count
|
764
|
+
end
|
765
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: z_build
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 1
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 1.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Shaun Bruno
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2013-03-31 00:00:00 -07:00
|
18
|
+
default_executable:
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description: Ditch your unweildy and heavyset build. Use Z'Build module with Rake/Thor/etc and make a light-weight alternative, written in pure Ruby.
|
22
|
+
email: shaun.bruno@gmail.com
|
23
|
+
executables: []
|
24
|
+
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files: []
|
28
|
+
|
29
|
+
files:
|
30
|
+
- lib/z_build.rb
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: https://github.com/shaunbruno/z_build
|
33
|
+
licenses: []
|
34
|
+
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options: []
|
37
|
+
|
38
|
+
require_paths:
|
39
|
+
- lib
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
segments:
|
52
|
+
- 0
|
53
|
+
version: "0"
|
54
|
+
requirements: []
|
55
|
+
|
56
|
+
rubyforge_project:
|
57
|
+
rubygems_version: 1.3.6
|
58
|
+
signing_key:
|
59
|
+
specification_version: 3
|
60
|
+
summary: Z'Build Module
|
61
|
+
test_files: []
|
62
|
+
|