z_build 1.1.0

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