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