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.
Files changed (2) hide show
  1. data/lib/z_build.rb +765 -0
  2. 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
+