metabuild 0.3

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 (6) hide show
  1. data/Manifest +4 -0
  2. data/README +5 -0
  3. data/Rakefile +11 -0
  4. data/lib/metabuild.rb +1016 -0
  5. data/metabuild.gemspec +30 -0
  6. metadata +72 -0
@@ -0,0 +1,4 @@
1
+ Manifest
2
+ README
3
+ Rakefile
4
+ lib/metabuild.rb
data/README ADDED
@@ -0,0 +1,5 @@
1
+ MetaBuild is a ruby "make-like" tool that builds components.
2
+ It supports tracking of dependencies among components, and a hierarchical reporting facility.
3
+
4
+
5
+ See metabuild.rubyforge.org
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+
5
+ Echoe.new('metabuild', '0.3') do |s|
6
+ s.author = "F. Brault"
7
+ s.email = "castorpilot@yahoo.com"
8
+ s.description = "MetaBuild is a ruby make-like tool that builds components. It supports tracking of dependencies among components, and a hierarchical reporting facility."
9
+ s.url = "http://metabuild.rubyforge.org/"
10
+ s.project = "metabuild"
11
+ end
@@ -0,0 +1,1016 @@
1
+ #!/usr/bin/ruby
2
+
3
+ #
4
+ # MetaBuild is a ruby "make-like" tool that builds components.
5
+ #
6
+ # Author:: F. Brault
7
+ # Copyright:: Copyright (c) 2010 F. Brault
8
+ # License:: GPL
9
+ #
10
+ # This program is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # (at your option) any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU General Public License
21
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
+ #
23
+ # For further information, see http://rubyforge.org/projects/metabuild/
24
+ #
25
+
26
+ require 'optparse'
27
+ require 'fileutils'
28
+ require 'digest/sha1'
29
+ require 'forwardable'
30
+ require 'tempfile'
31
+
32
+ include FileUtils
33
+
34
+ # This module defines a "meta-make" system in ruby.
35
+ # The goal is to create build directive for a component,
36
+ # along with different targets an depedencies among them ( like in Make).
37
+ # A hierarchical reporting tool is included.
38
+ module Metabuild
39
+
40
+ CACHE = ENV.fetch("RBUILD_CACHE", "/work/common/build_cache")
41
+
42
+ DEFAULT_TAGS = [
43
+ lambda {`uname -p`},
44
+ lambda {`uname -o`},
45
+ lambda {File.read("/etc/fedora-release")},
46
+ lambda {s = ""; 1.upto(ARGV.length-1) {|i| s += ARGV[i]};s}
47
+ ]
48
+
49
+ DEFAULT_OPTIONS = {
50
+ "clone" => [".", "Path of the SCM clone"],
51
+ "cache" => ["no", "Use caching to speed up builds"],
52
+ "dependency" => ["yes", "Track dependencies between targets"],
53
+ "workspace" => ENV.fetch("WORKSPACE", Dir.pwd),
54
+ "logtype" => "text",
55
+ "logfile" => "",
56
+ "dep_graph" => ["no", "Output Dependency graph in Graphviz dot format"],
57
+ "parallel" => ["no", "Run threads in parallel. Make sure that your dependency graph can handle this !"],
58
+ }
59
+
60
+ # Function to extend fileutils
61
+
62
+ # helper function that does (rm -rf dir; mkdir dir; cd dir)
63
+ def create_goto_dir!(dir)
64
+ rm_rf dir
65
+ mkdir_p dir
66
+ cd dir
67
+ end
68
+
69
+ # helper function that does goes into dir if it exists, or create it and goes into it
70
+ def cd!(dir)
71
+ mkdir_p dir unless File.directory? dir
72
+ cd dir
73
+ end
74
+
75
+
76
+ # Mother class for all SCM access, "virtual"
77
+ class SCM
78
+
79
+ attr_accessor :path
80
+
81
+ def initialize(path)
82
+ @path = path
83
+ end
84
+
85
+ def cksum
86
+ raise "Should be implemented in child class"
87
+ end
88
+
89
+ def get_diff
90
+ raise "Should be implemented in child class"
91
+ end
92
+
93
+ def apply_patch(patch)
94
+ raise "Should be implemented in child class"
95
+ end
96
+
97
+ def clone(path)
98
+ raise "Should be implemented in child class"
99
+ end
100
+
101
+ end
102
+
103
+ # Git access, uses Grit ruby lib
104
+ class Git < SCM
105
+
106
+ def initialize(path)
107
+ super
108
+ end
109
+
110
+ # Like running "git diff" in a git repo
111
+ def get_diff
112
+ `cd #{path} && git diff`
113
+ end
114
+
115
+ def apply_patch(patch)
116
+ pipe = IO.popen("git apply -")
117
+ pipe.puts patch
118
+ pipe.close_write
119
+ end
120
+
121
+ def clone(path)
122
+ raise "unable to clone #{path}" unless system("git clone " + path)
123
+ end
124
+
125
+ # id is the SHA1 of head + of any diff (locals)
126
+ def cksum
127
+ `cd #{path};git log --pretty=format:'%H' | head -n 1`.to_i(16) ^ Digest::SHA1.hexdigest(get_diff).to_i(16)
128
+ end
129
+
130
+ end
131
+
132
+ # Mother class for reporting. Defines a simple text-based logfile
133
+ class Log
134
+
135
+ attr_accessor :name
136
+
137
+ def initialize(name)
138
+ @name = name
139
+ @subscript = (defined? $subscript_log and not $subscript_log.nil?) ? true : false
140
+ end
141
+
142
+ # Helper while waiting for Ruby 1.9 and the "env" option in _system_
143
+ def env_system(cmd, env)
144
+ str = ""
145
+ env.each { |k, v| str += "export #{k}=#{v};"}
146
+ system(str + cmd)
147
+ end
148
+
149
+ # Run a shell command, log if it fails (override in child class)
150
+ def run(command, env, fail_msg)
151
+ env_system(command, env)
152
+ end
153
+
154
+ # Run a shell command, no logging (override in child class)
155
+ def silent(command, env)
156
+ env_system(command, env)
157
+ end
158
+
159
+ # Run a shell command. Log success +and+ failure (override in child class)
160
+ def valid(command, env, fail_msg, success_msg)
161
+ env_system(command, env)
162
+ end
163
+
164
+ # Spawn with env
165
+ def env_spawn(cmd, env, fail_msg, success_msg)
166
+ str = ""
167
+ env.each { |k, v| str += "export #{k}=#{v};"}
168
+ pid = spawn(str + cmd)
169
+ return [pid, fail_msg]
170
+ end
171
+
172
+ def parallel_wait(p)
173
+ pid, stat = Process::waitpid2(p[0])
174
+ failure p[1] unless stat.success?
175
+ end
176
+
177
+ # Run another ruby build script
178
+ # Be careful with global variables !!!
179
+ # By default, the default options values are transmitted to the subscript
180
+ def script(script, args, options, fail_msg, use_default)
181
+ $subscript_argv = args.split
182
+ options.each {|k,v| $subscript_argv.push "--#{k}=#{v}" if (DEFAULT_OPTIONS.has_key?(k) and not args =~ /--#{k}=/)} if use_default
183
+ $subscript_log = ""
184
+ begin
185
+ load(script, true)
186
+ rescue Exception => exception
187
+ integrate $subscript_log if $subscript_log != ""
188
+ if exception.kind_of? SystemExit
189
+ failure fail_msg unless exception.success?
190
+ else
191
+ failure(fail_msg + "(#{exception.class}: #{exception.message})\n" + exception.backtrace.join("\n"))
192
+ end
193
+ end
194
+ integrate $subscript_log
195
+ $subscript_log = nil
196
+ $subscript_argv = nil
197
+ end
198
+
199
+ def failure(f)
200
+ exit false
201
+ end
202
+
203
+ def integrate log
204
+ raise "Should be implemented in child class"
205
+ end
206
+
207
+ def finish
208
+ exit true
209
+ end
210
+
211
+ def status
212
+ false
213
+ end
214
+
215
+ end
216
+
217
+ # Simple log that just prints out text (for console builds)
218
+ class LogText < Log
219
+
220
+ attr_reader :name, :success, :fail
221
+
222
+ def initialize(name)
223
+ super(name)
224
+ @fail, @success = [], []
225
+ @subscripts = []
226
+ @report_done = false
227
+ end
228
+
229
+ def add_fail(msg)
230
+ @fail.push msg
231
+ end
232
+
233
+ def add_success(msg)
234
+ @success.push msg
235
+ end
236
+
237
+ def report
238
+ return if @report_done # prevent reporting several times
239
+ if @subscript
240
+ $subscript_log = self
241
+ else
242
+ output
243
+ end
244
+ @report_done = true
245
+ end
246
+
247
+ def output
248
+ puts "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
249
+ puts @name
250
+ puts "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@"
251
+ puts "Ran the following subscripts :"
252
+ @subscripts.each {|s| puts s}
253
+ puts "----------------------------------------------"
254
+ puts "Sucess : "
255
+ @success.each {|s| puts s}
256
+ puts "----------------------------------------------"
257
+ puts "Fail : "
258
+ @fail.each {|s| puts s}
259
+ puts "----------------------------------------------"
260
+ end
261
+
262
+ def status
263
+ @fail.empty?
264
+ end
265
+
266
+ # Run a shell command, log if it fails
267
+ def run(command, env, fail_msg)
268
+ failure fail_msg unless env_system(command, env)
269
+ end
270
+
271
+ # Run a shell command, no logging
272
+ def silent(command, env)
273
+ stop unless env_system(command, env)
274
+ end
275
+
276
+ # Run a shell command. Log success +and+ failure
277
+ def valid(command, env, fail_msg, success_msg)
278
+ if env_system(command, env)
279
+ add_success success_msg if success_msg != ""
280
+ else
281
+ add_fail fail_msg
282
+ end
283
+ end
284
+
285
+ def integrate log
286
+ @subscripts.push log.name
287
+ log.success.each {|s| @success.push("[#{log.name}] " + s)}
288
+ log.fail.each {|f| @fail.push("[#{log.name}] " + f)}
289
+ end
290
+
291
+ # Stop script, adding a message to the log
292
+ def failure(msg)
293
+ add_fail msg
294
+ stop
295
+ end
296
+
297
+ # Output log and exit with error exit status
298
+ def stop
299
+ report
300
+ exit false
301
+ end
302
+
303
+ def finish
304
+ report
305
+ exit status
306
+ end
307
+
308
+ end
309
+
310
+ # Html log. For reporting in continuous integration systems, for example.
311
+ class LogHtml < Log
312
+
313
+ attr_reader :name, :html, :success, :fail
314
+
315
+ def initialize(name, file)
316
+ super(name)
317
+ @file = file
318
+ @fail, @success = [], []
319
+ @subscripts = []
320
+ @report_done = false
321
+ @html = ""
322
+ @has_bash = system("bash --version > /dev/null")
323
+ end
324
+
325
+ def add_fail(cmd, msg, output)
326
+ @fail.push [cmd, msg, output]
327
+ end
328
+
329
+ def add_success(cmd, msg, output)
330
+ @success.push [cmd, msg, output]
331
+ end
332
+
333
+ def report
334
+ return if @report_done # prevent reporting several times
335
+ fill_html
336
+ if @subscript
337
+ $subscript_log = self
338
+ else
339
+ output
340
+ end
341
+ @report_done = true
342
+ end
343
+
344
+ def fill_html
345
+
346
+ top = "<div class='raised'>\n" +
347
+ "<b class='b1'></b><b class='b2'></b><b class='b3'></b><b class='b4'></b><div class='boxcontent'>\n"
348
+ bottom = "</div><b class='b4b'></b><b class='b3b'></b><b class='b2b'></b><b class='b1b'></b></div>\n"
349
+ red = "<img src='data:image/png;base64,
350
+ iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAAXNSR0IArs4c6QAAAWdJREFUOMul
351
+ VD1zgkAUfF48nDmTIZPEZM6CxgZrCitqKwsaK3+elY2NjY2NFhZQyy8AE5jMkMSLQgIpjjn8jKBb
352
+ Hrf79i3vXsk0TbgO5cOjUhRVXFdyHMl1se8DQIJxSGlI6YbSqFY7I4EYk+fz+8mk4jjScok9j0ts
353
+ KA3r9e9G473dZs3mTsntRspB8DgaPYzHZLE46jkm5KPV8judQNePuCgHwdNw+DwY8MpHgRiTZzPe
354
+ nVBB2bfp9H++SIrY9ku/L5wiflq17Tx8obJ9HwHADWO3lnWq/1Md3VkWse1UAjFWLcIXKpyVuuB6
355
+ hSBYaRY5U9hLJMviSiA+fIdjexaChQDglxCmqkUlBAvxsV3tjn0eCFbq4kvTWBGVmJBPTctcJBiv
356
+ VPWt282ZyN59JFQDXc+jkmDMVPW11xOus5f6I8u+YSQYX/7YuYpnGCGll6+cw8UneR4AxJJUYPHx
357
+ mmtFWStKzr/zB1QX2NNv3dXcAAAAAElFTkSuQmCC'>"
358
+ green = "<img src='data:image/png;base64,
359
+ iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAIAAABL1vtsAAAAAXNSR0IArs4c6QAAAWhJREFUOMul
360
+ lMGKwjAQhsdUUlq2UCIs23oVb148eNUH8AHWJ9wHWB+gexXx0lvx2mQRHIQslgaFPaRkS+tqiv8x
361
+ zf/1ZzIzve12C8+p3z6STiFc5BQFRekUABBcvUixWLGoZMHVe4DgFJMw3QV7TlG4NUTJYsWmcrQ4
362
+ TWLF/kVkfr4ebJIw5RQbuaSfZ36e+fm3i8vjbHwe3kBkfv7x+pWEqf7zTXGKn4ONdIrVYW4oxHzT
363
+ /7/jN4mSMF0PNiYpMac2/jrF3CcAIFzU9bN/SE5xF+yFixWCU+zkNxTtIgAgKGpeJwkXhUFIp7Cs
364
+ QvOlTS2eFNHN127bhzIuAgCRYlHJuiKikkWKVYhYsUbb28i4iOZN5agTRY+czl7VYnGaLE4Ty4o0
365
+ 7hNDXR5nNhTtXx5nJvXfpI7Pw9VhHly99rDX82v/7WHXlPfL/K1knVZOr707zeLjFH/6BQC8XLzY
366
+ fvFVPXMe1qPe1y/ZDt7tn1jpQgAAAABJRU5ErkJggg=='>"
367
+
368
+ unless @subscript
369
+ @html << "<html>\n"
370
+ @html << "<head>\n"
371
+ @html << "<script type='text/javascript'>\n"
372
+ @html << "function toggleMe(a){\n"
373
+ @html << "var e=document.getElementById(a);\n"
374
+ @html << "if(!e)return true;\n"
375
+ @html << "if(e.style.display=='none'){\n"
376
+ @html << "e.style.display='block'\n"
377
+ @html << "} else {\n"
378
+ @html << "e.style.display='none'\n"
379
+ @html << "}\n"
380
+ @html << "return true;\n"
381
+ @html << "}\n"
382
+ @html << "</script>\n"
383
+ @html << "<style type='text/css'>\n"
384
+ @html << "A"
385
+ @html << "{"
386
+ @html << " color:white;"
387
+ @html << "}"
388
+ @html << "A:link {color:#000000}"
389
+ @html << "A:visited {color:#ffffff}"
390
+ @html << "A:active {color:#ffffff}"
391
+ @html << "A:hover {background-color: black; color: white}"
392
+ @html << "div.header"
393
+ @html << "{"
394
+ @html << " text-align: center;"
395
+ @html << " margin-left: 100px;"
396
+ @html << " margin-right: 100px;"
397
+ @html << " margin-bottom: 50px;"
398
+ @html << " float: center;"
399
+ @html << "}"
400
+ @html << "div.content"
401
+ @html << "{"
402
+ @html << " float:center;"
403
+ @html << " margin-left:100px;"
404
+ @html << " margin-right:100px;"
405
+ @html << "}"
406
+ @html << "div.output"
407
+ @html << "{"
408
+ @html << " float:center;"
409
+ @html << " margin-left:100px;"
410
+ @html << " margin-right:100px;"
411
+ @html << " border-style: dashed;"
412
+ @html << "}"
413
+ @html << ".raised {"
414
+ @html << " background:transparent; "
415
+ @html << " width:100%;"
416
+ @html << "}"
417
+ @html << ".raised h1, .raised p {"
418
+ @html << " margin:0 10px;"
419
+ @html << " }"
420
+ @html << ".raised h1 {"
421
+ @html << " font-size:2em; "
422
+ @html << " }"
423
+ @html << ".raised p {"
424
+ @html << " padding-bottom:0.5em;"
425
+ @html << " }"
426
+ @html << ".raised .b1, .raised .b2, .raised .b3, .raised .b4, .raised .b1b, .raised .b2b, .raised .b3b, .raised .b4b {"
427
+ @html << " display:block; "
428
+ @html << " overflow:hidden;"
429
+ @html << " font-size:1px;"
430
+ @html << " }"
431
+ @html << ".raised .b1, .raised .b2, .raised .b3, .raised .b1b, .raised .b2b, .raised .b3b {"
432
+ @html << " height:1px;"
433
+ @html << " }"
434
+ @html << ".raised .b2 {"
435
+ @html << " background:#ccc; "
436
+ @html << " border-left:1px solid #fff; "
437
+ @html << " border-right:1px solid #eee;"
438
+ @html << " }"
439
+ @html << ".raised .b3 {"
440
+ @html << " background:#ccc; "
441
+ @html << " border-left:1px solid #fff; "
442
+ @html << " border-right:1px solid #ddd;"
443
+ @html << " }"
444
+ @html << ".raised .b4 {"
445
+ @html << " background:#ccc; "
446
+ @html << " border-left:1px solid #fff; "
447
+ @html << " border-right:1px solid #aaa;"
448
+ @html << " }"
449
+ @html << ".raised .b4b {"
450
+ @html << " background:#ccc; "
451
+ @html << " border-left:1px solid #eee; "
452
+ @html << " border-right:1px solid #999;"
453
+ @html << " }"
454
+ @html << ".raised .b3b {"
455
+ @html << " background:#ccc; "
456
+ @html << " border-left:1px solid #ddd; "
457
+ @html << " border-right:1px solid #999;"
458
+ @html << " }"
459
+ @html << ".raised .b2b {"
460
+ @html << " background:#ccc; "
461
+ @html << " border-left:1px solid #aaa; "
462
+ @html << " border-right:1px solid #999;"
463
+ @html << " }"
464
+ @html << ".raised .b1 {"
465
+ @html << " margin:0 5px; "
466
+ @html << " background:#fff;"
467
+ @html << " }"
468
+ @html << ".raised .b2, .raised .b2b {"
469
+ @html << " margin:0 3px; "
470
+ @html << " border-width:0 2px;"
471
+ @html << " }"
472
+ @html << ".raised .b3, .raised .b3b {"
473
+ @html << " margin:0 2px;"
474
+ @html << " }"
475
+ @html << ".raised .b4, .raised .b4b {"
476
+ @html << " height:2px; margin:0 1px;"
477
+ @html << " }"
478
+ @html << ".raised .b1b {"
479
+ @html << " margin:0 5px; background:#999;"
480
+ @html << " }"
481
+ @html << ".raised .boxcontent {"
482
+ @html << " display:block; "
483
+ @html << " background:#ccc; "
484
+ @html << " border-left:1px solid #fff; "
485
+ @html << " border-right:1px solid #999;"
486
+ @html << " }"
487
+ @html << "</style>\n"
488
+ @html << "</head><body bgcolor='grey'>\n"
489
+ end
490
+
491
+ @html << "<div class='header'>\n"
492
+ @html << top
493
+ @html << "<h1>#{@name} "
494
+ if status
495
+ @html << green + "</h1><br>\n"
496
+ else
497
+ @html << red + "</h1><br>\n"
498
+ end
499
+ @html << bottom + "</div>"
500
+
501
+ @html << "<div class='content'>\n"
502
+ @html << top
503
+ @html << "<p><h2>Subscript run :</h2><br>\n"
504
+ id = (rand() * 500).to_i # FIXME
505
+ @subscripts.each do |s|
506
+ name, status, html = s[0], s[1], s[2]
507
+ stat = status ? green : red
508
+ @html << "<div id='para#{id}' style='display:none'>\n"
509
+ @html << "<div class='output'>\n"
510
+ @html << html
511
+ @html << "</div>"
512
+ @html << "</div>\n"
513
+ @html << "<br>#{stat} #{name}<input type='button' class='button' onclick='return toggleMe(\"para#{id}\")' value='Show Html log'>\n"
514
+ id += 1
515
+ end
516
+ @html << "<br></p>\n"
517
+
518
+ @html << "<p><h2>Tests :</h2><br>\n"
519
+
520
+ @fail.each do |s|
521
+ cmd, msg, output = s[0], s[1], s[2]
522
+ @html << "<div id='para#{id}' style='display:none'>\n"
523
+ @html << "<div class='output'>\n"
524
+ @html << "<pre>" + output + "</pre>"
525
+ @html << "</div>"
526
+ @html << "</div>\n"
527
+ @html << "<br>#{red} #{msg}\n"
528
+ @html << "<input type='button' class='button' onclick='return toggleMe(\"para#{id}\")' value='Show output'>\n"
529
+ id += 1
530
+ end
531
+
532
+ @success.each do |s|
533
+ cmd, msg, output = s[0], s[1], s[2]
534
+ @html << "<div id='para#{id}' style='display:none'>\n"
535
+ @html << "<div class='output'>\n"
536
+ @html << "<pre>" + output + "</pre>"
537
+ @html << "</div>"
538
+ @html << "</div>\n"
539
+ @html << "<br>#{green} #{msg}\n"
540
+ @html << "<input type='button' class='button' onclick='return toggleMe(\"para#{id}\")' value='Show output'>\n"
541
+ id += 1
542
+ end
543
+
544
+ @html << "<br></p>\n"
545
+
546
+ @html << bottom + "</div>"
547
+
548
+ unless @subscript
549
+ @html << "</body></html>\n"
550
+ end
551
+
552
+ end
553
+
554
+ def output
555
+ File.open(@file, "w") do |f|
556
+ f.puts @html
557
+ end
558
+ end
559
+
560
+ def status
561
+ @fail.empty?
562
+ end
563
+
564
+ def setup_script(cmd, env)
565
+ file = Tempfile.new("output")
566
+ file.close
567
+ script = Tempfile.new("script")
568
+ if @has_bash
569
+ script.puts "exec > >(tee #{file.path}) 2>&1" # Redirect output to tmpfile + tee
570
+ else
571
+ script.puts "exec > #{file.path} 2>&1" # Redirect output to tmpfile
572
+ end
573
+ env.each { |k, v| script.puts "export #{k}=#{v}"}
574
+ script.puts cmd
575
+ script.close
576
+ return script.path, file.path
577
+ end
578
+
579
+ def env_system(cmd, env)
580
+ script, file = setup_script(cmd, env)
581
+ puts "@@@@@@@@@@@ Running #{cmd} @@@@@@@@@@@@@"
582
+ shell = @has_bash ? "bash " : "sh "
583
+ success = system(shell + script)
584
+ output = File.new(file).read.strip
585
+ File.delete script
586
+ File.delete file
587
+ return success, output
588
+ end
589
+
590
+ def env_spawn(cmd, env, fail_msg = "", success_msg = "")
591
+ script, file, shell = setup_script(cmd, env)
592
+ puts "@@@@@@@@@@@ Running #{cmd} in background @@@@@@@@@@@@@"
593
+ pid = spawn(shell + script)
594
+ return [pid, file, script, cmd, fail_msg, success_msg]
595
+ end
596
+
597
+ def parallel_wait(p)
598
+ pid, file, script, cmd, fail_msg, success_msg= p[0], p[1], p[2], p[3], p[4], p[5]
599
+ pid, stat = Process::waitpid2(pid)
600
+ success = stat.success?
601
+ output = File.new(file).read.strip
602
+ File.delete script
603
+ File.delete file
604
+ if success
605
+ add_success cmd, success_msg, output
606
+ else
607
+ add_fail cmd, fail_msg, output
608
+ end
609
+ end
610
+
611
+
612
+ # Run a shell command, log if it fails
613
+ def run(command, env, fail_msg)
614
+ success, output= env_system(command, env)
615
+ unless success
616
+ add_fail command, fail_msg, output
617
+ stop
618
+ end
619
+ end
620
+
621
+ # Run a shell command, no logging
622
+ def silent(command, env)
623
+ success, output = env_system(command, env)
624
+ stop unless success
625
+ end
626
+
627
+ # Run a shell command. Log success +and+ failure
628
+ def valid(command, env, fail_msg, success_msg)
629
+ success, output = env_system(command, env)
630
+ if success
631
+ add_success command, success_msg, output
632
+ else
633
+ add_fail command, fail_msg, output
634
+ end
635
+ end
636
+
637
+ def integrate log
638
+ if log.kind_of? LogHtml
639
+ @subscripts.push [log.name, log.status, log.html]
640
+ else
641
+ @subscripts.push [log.name, log.status, ""]
642
+ end
643
+ end
644
+
645
+ # Stop script, adding a message to the log
646
+ def failure(msg, cmd = "", output = "")
647
+ add_fail cmd, msg, output
648
+ stop
649
+ end
650
+
651
+ # Output log and exit with error exit status
652
+ def stop
653
+ report
654
+ exit false
655
+ end
656
+
657
+ def finish
658
+ report
659
+ exit status
660
+ end
661
+
662
+ end
663
+ # A Target is really at the heart of a build. It handles dependencies, caching ...
664
+ class Target
665
+
666
+ attr_accessor :name, :repo, :tags, :results, :depends, :block, :done
667
+
668
+ def initialize(name, repo, depends = [], results = [], tags = [])
669
+ @name, @repo, @tags, @results, @depends = name, repo, tags, results, depends
670
+ @done = false
671
+ @block = lambda { raise "Target #{@name} has empty code block !" }
672
+ end
673
+
674
+ def to_s
675
+ @name
676
+ end
677
+
678
+ def add_result res
679
+ @results.push res
680
+ end
681
+
682
+ def add_dep dep
683
+ if dep.kind_of? Array
684
+ @depends = @depends | dep
685
+ else
686
+ @depends.push dep
687
+ end
688
+ end
689
+
690
+ def remove_dep dep
691
+ @depends.delete dep
692
+ end
693
+
694
+ def each_dep
695
+ @depends.each do |d|
696
+ yield d
697
+ end
698
+ end
699
+
700
+ def each_result
701
+ @results.each do |r|
702
+ yield r
703
+ end
704
+ end
705
+
706
+ # The checksum is a mix of the repository checksum + the default tags + local tags
707
+ # Default tags include processor type, kernel name, etc.
708
+ def cksum
709
+ DEFAULT_TAGS.concat(@tags).inject(@repo.cksum) { |sum, tag|
710
+ Digest::SHA1.hexdigest(tag.call).to_i(16) ^ sum
711
+ }.to_s(16)
712
+ end
713
+
714
+ def check_cache
715
+ File.exist?(CACHE + cksum + ".tgz")
716
+ end
717
+
718
+ def get_cache
719
+ cmd = "tar zxf " + CACHE + "/" + cksum + ".tgz"
720
+ raise "Error getting target #{@name} in cache" unless system(cmd)
721
+ end
722
+
723
+ def update_cache
724
+ cmd = ":;" + @results.inject("tar zcf " + CACHE + "/" + cksum + ".tgz "){|s, r| s += "#{r} "}
725
+ raise "Error tar for target #{@name}" unless system(cmd)
726
+ end
727
+
728
+ # Run a block that defines all the steps of the build.
729
+ # Use caching if asked to.
730
+ # +Note+ : Whatever the directory changes happening in the block,
731
+ # when the block exits, we are in the same directory as when it started
732
+ def run(use_cache = false)
733
+ return if @done
734
+ old_dir = pwd
735
+ if use_cache
736
+ if check_cache
737
+ puts "getting from cache"
738
+ get_cache
739
+ else
740
+ block.call self
741
+ update_cache
742
+ end
743
+ else
744
+ block.call self
745
+ end
746
+ cd old_dir
747
+ @done = true
748
+ end
749
+
750
+ # Run according to dependency list, from bottom to top
751
+ def propagate_run(use_cache=false, parallel=false)
752
+ if parallel
753
+ each_dep do |d|
754
+ fork {d.propagate_run(use_cache, parallel)}
755
+ end
756
+ Process.waitall unless @depends.empty?
757
+ else
758
+ each_dep do |d|
759
+ d.propagate_run(use_cache, parallel)
760
+ end
761
+ end
762
+ run(use_cache)
763
+ end
764
+
765
+ def propagate_disable
766
+ each_dep do |d|
767
+ d.done = true
768
+ d.propagate_disable
769
+ end
770
+ end
771
+
772
+ def output_graph
773
+ each_dep do |d|
774
+ puts "#{@name} -> #{d.name};"
775
+ end
776
+ end
777
+
778
+ end
779
+
780
+ # A class for parsing command line options
781
+ class Options
782
+
783
+ extend Forwardable
784
+ attr_reader :opts, :rest, :from_targets
785
+ def_delegators :@opts, :[], :fetch, :each
786
+
787
+ # By default, the following options are set : --help, and DEFAULT_OPTIONS.
788
+ # So caching and target dependencies tracking are disabled by default.
789
+ # h is a hashmap of tuples option_name => default_value
790
+ def initialize(h)
791
+ @opts = {}
792
+ @help = false
793
+ hash = DEFAULT_OPTIONS.merge(h)
794
+ @parser = OptionParser.new
795
+ @parser.on("--help") {|val| @help = true}
796
+ @from_targets = []
797
+ @parser.on("--from=[target]", "Disable everything below target in dep tree. Only with dependency=yes") {|val| @from_targets.push val}
798
+ hash.each do |opt, default|
799
+ if default.kind_of? Array
800
+ @parser.on("--#{opt}=[#{default[0]}]", default[1]) {|val| @opts[opt] = val}
801
+ else
802
+ @parser.on("--#{opt}=[#{default}]") {|val| @opts[opt] = val}
803
+ end
804
+ end
805
+ if defined? $subscript_argv and not $subscript_argv.nil?
806
+ argv = $subscript_argv
807
+ else
808
+ argv = ARGV
809
+ end
810
+ @rest = @parser.parse(*argv)
811
+ hash.each do |opt, default|
812
+ default = default[0] if default.kind_of? Array
813
+ @opts[opt] = default unless @opts.has_key? opt
814
+ end
815
+ end
816
+
817
+ def help?
818
+ @help
819
+ end
820
+
821
+ def help
822
+ @parser.to_s
823
+ end
824
+
825
+ end
826
+
827
+ # The builder class is responsible for triggerring targets and reporting failures or
828
+ # exceptions via the logging facilities
829
+ class Builder
830
+
831
+ extend Forwardable
832
+ attr_accessor :name, :targets, :default_targets, :env
833
+ def_delegators :@log, :finish
834
+
835
+ def initialize(name, options = nil, targets = [])
836
+ @name, @options, @targets = name, options, targets
837
+ raise "Builder needs Options (none given)!" if options.nil?
838
+ title = "Report for #{name}"
839
+ @log = case @options.fetch("logtype", "text")
840
+ when "text" then LogText.new(title)
841
+ when "html"
842
+ raise "Need a logfile for html reports" if @options["logfile"].nil? or @options["logfile"].empty?
843
+ LogHtml.new(title, @options["logfile"])
844
+ else raise "Unknown log type #{@options["logtype"]}"
845
+ end
846
+ @default_targets = []
847
+ @env = {"WORKSPACE" => @options.fetch("workspace", Dir.pwd)}
848
+ @subprocesses = []
849
+ end
850
+
851
+ def logtitle=(title)
852
+ @log.name = title
853
+ end
854
+
855
+ def logtitle
856
+ @log.name
857
+ end
858
+
859
+ # Run targets according to what the user specified in ARGV
860
+ # Dependencies among targets can be propagated or not (default = no)
861
+ # Outpout log and stop
862
+ def launch
863
+ begin
864
+
865
+ if @options.help?
866
+ puts @options.help
867
+ puts "Default targets are built if no targets are given as argument. You can specify any of the targets listed below"
868
+ puts "Default targets :"
869
+ @default_targets.each { |t| puts t.name }
870
+ puts "The following targets are defined :"
871
+ @targets.each { |t| puts t.name }
872
+ exit
873
+ end
874
+
875
+ if @options.fetch("dep_graph", "no").downcase == "yes"
876
+ output_graph
877
+ end
878
+
879
+ @options.from_targets.each do |targ|
880
+ get_target(targ).propagate_disable
881
+ end
882
+
883
+ use_cache = (@options.fetch("cache", "no").downcase == "yes")
884
+ parallel = (@options.fetch("parallel", "no").downcase == "yes")
885
+ targs = @options.rest.empty? ? @default_targets.map {|t| t.to_s} : @options.rest
886
+ if @options.fetch("dependency", "yes").downcase != "yes"
887
+ targs.each { |t| targ = get_target(t); targ.run(use_cache) unless targ.nil? }
888
+ else
889
+ targs.each { |t| targ = get_target(t); targ.propagate_run(use_cache, parallel) unless targ.nil? }
890
+ end
891
+ @log.finish
892
+ rescue Exception => exception
893
+ # Uses @report_done in Log to prevent reporting several times
894
+ if exception.kind_of? SystemExit
895
+ @log.failure "Error : target exited with failure code" unless exception.success?
896
+ else
897
+ @log.failure("Error in target : (#{exception.class}: #{exception.message})\n" + exception.backtrace.join("\n"))
898
+ end
899
+ end
900
+ end
901
+
902
+ # Output dependencies between targets in Graphviz dot format
903
+ def output_graph
904
+ puts "digraph Dependencies {"
905
+ @targets.each {|t| puts "node [shape=ellipse];#{t.name};"}
906
+ @targets.each {|t| t.output_graph}
907
+ puts "}"
908
+ exit
909
+ end
910
+
911
+ # Run another ruby build script
912
+ # Be careful with global variables !!!
913
+ # By default, the default options values are transmitted to the subscript
914
+ def script(script, args, fail_msg = "FAILED running #{script} #{args}", use_default = true)
915
+ @log.script(script, args, @options, fail_msg, use_default)
916
+ end
917
+
918
+ # Run a shell command, giving it (optional) env variables (a hashmap), and (optional) failure message
919
+ # Only log failures.
920
+ # :call-seq:
921
+ # run(:cmd => "cmd", :env => env, :msg => "msg") -> nil
922
+ # run("cmd") -> nil
923
+ def run(h)
924
+ if h.kind_of? String
925
+ @log.run(h, @env, "#{h} FAIL")
926
+ else
927
+ raise "run command needs a non nil command" if h[:cmd].nil?
928
+ @log.run(h[:cmd], @env.merge(h.fetch(:env, @env)), h.fetch(:msg, "#{h[:cmd]} FAIL"))
929
+ end
930
+ end
931
+
932
+ # Run a shell command, no logging. Give it (optional) env variables (hashmap)
933
+ # :call-seq:
934
+ # silent(:cmd => "cmd", :env => env) -> nil
935
+ # silent("cmd") -> nil
936
+ def silent(h)
937
+ if h.kind_of? String
938
+ @log.silent(h, @env)
939
+ else
940
+ raise "silent command needs a non nil command" if h[:cmd].nil?
941
+ @log.silent(h[:cmd], @env.merge(h.fetch(:env, {})))
942
+ end
943
+ end
944
+
945
+ # Run a shell command, giving it (optional) env variables (a hashmap), and (optional) failure message
946
+ # Log success +and+ failure.
947
+ # :call-seq:
948
+ # valid(:cmd => "cmd", :env => env, :fail_msg => "msg", :success_msg => "msg") -> nil
949
+ # valid("cmd") -> nil
950
+ def valid(h)
951
+ if h.kind_of? String
952
+ @log.valid(h, @env, "#{h} FAIL", "#{h} SUCCESS")
953
+ else
954
+ raise "valid command needs a non nil command" if h[:cmd].nil?
955
+ @log.valid(h[:cmd], @env.merge(h.fetch(:env, {})), h.fetch(:fail_msg, "#{h[:cmd]} FAIL"), h.fetch(:success_msg, "#{h[:cmd]} SUCCESS"))
956
+ end
957
+ end
958
+
959
+ # Run a shell command in subprocess, giving it (optional) env variables (a hashmap), and (optional) failure message
960
+ # Use parallel_wait on the returned object _P_ .
961
+ # :call-seq:
962
+ # parallel_valid(:cmd => "cmd", :env => env, :fail_msg => "msg", :success_msg => "msg") -> P
963
+ # parallel_valid("cmd") -> P
964
+ def parallel_valid(h)
965
+ if h.kind_of? String
966
+ p = @log.env_spawn(h, @env, "#{h} FAIL", "#{h} SUCCESS")
967
+ else
968
+ raise "valid command needs a non nil command" if h[:cmd].nil?
969
+ p = @log.env_spawn(h[:cmd], @env.merge(h.fetch(:env, {})), h.fetch(:fail_msg, "#{h[:cmd]} FAIL"), h.fetch(:success_msg, "#{h[:cmd]} SUCCESS"))
970
+ end
971
+ @subprocess.push p
972
+ return p
973
+ end
974
+
975
+ # Wait for a subprocess, Log success +and+ failure (like valid)
976
+ def parallel_wait(p)
977
+ @log.parallel_wait(p)
978
+ @subprocess.delete(p)
979
+ end
980
+
981
+ # Wait for a all subprocess, Log success +and+ failure (like valid)
982
+ def parallel_waitall
983
+ @subprocess.each do |p|
984
+ @log.parallel_wait(p)
985
+ end
986
+ @subprocess = []
987
+ end
988
+
989
+ # Add target, either a single one, or an array of targets
990
+ def add_target(target)
991
+ if target.kind_of? Array
992
+ @targets = @targets | target
993
+ else
994
+ @targets.push target
995
+ end
996
+ end
997
+
998
+ def remove_target(target)
999
+ @targets.delete target
1000
+ end
1001
+
1002
+ def get_target(name)
1003
+ @targets.each do |t|
1004
+ return t if t.name == name
1005
+ end
1006
+ raise "Unkonwn target #{name} for build #{@name}"
1007
+ end
1008
+
1009
+ # Set a block of code to be run as target, catching doing logging
1010
+ def target(target, &block)
1011
+ get_target(target).block = block
1012
+ end
1013
+
1014
+ end
1015
+
1016
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{metabuild}
5
+ s.version = "0.3"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["F. Brault"]
9
+ s.date = %q{2010-05-20}
10
+ s.description = %q{MetaBuild is a ruby make-like tool that builds components. It supports tracking of dependencies among components, and a hierarchical reporting facility.}
11
+ s.email = %q{castorpilot@yahoo.com}
12
+ s.extra_rdoc_files = ["README", "lib/metabuild.rb"]
13
+ s.files = ["Manifest", "README", "Rakefile", "lib/metabuild.rb", "metabuild.gemspec"]
14
+ s.homepage = %q{http://metabuild.rubyforge.org/}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Metabuild", "--main", "README"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{metabuild}
18
+ s.rubygems_version = %q{1.3.6}
19
+ s.summary = %q{MetaBuild is a ruby make-like tool that builds components. It supports tracking of dependencies among components, and a hierarchical reporting facility.}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ else
27
+ end
28
+ else
29
+ end
30
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metabuild
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 3
8
+ version: "0.3"
9
+ platform: ruby
10
+ authors:
11
+ - F. Brault
12
+ autorequire:
13
+ bindir: bin
14
+ cert_chain: []
15
+
16
+ date: 2010-05-20 00:00:00 +02:00
17
+ default_executable:
18
+ dependencies: []
19
+
20
+ description: MetaBuild is a ruby make-like tool that builds components. It supports tracking of dependencies among components, and a hierarchical reporting facility.
21
+ email: castorpilot@yahoo.com
22
+ executables: []
23
+
24
+ extensions: []
25
+
26
+ extra_rdoc_files:
27
+ - README
28
+ - lib/metabuild.rb
29
+ files:
30
+ - Manifest
31
+ - README
32
+ - Rakefile
33
+ - lib/metabuild.rb
34
+ - metabuild.gemspec
35
+ has_rdoc: true
36
+ homepage: http://metabuild.rubyforge.org/
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options:
41
+ - --line-numbers
42
+ - --inline-source
43
+ - --title
44
+ - Metabuild
45
+ - --main
46
+ - README
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 0
55
+ version: "0"
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ segments:
61
+ - 1
62
+ - 2
63
+ version: "1.2"
64
+ requirements: []
65
+
66
+ rubyforge_project: metabuild
67
+ rubygems_version: 1.3.6
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: MetaBuild is a ruby make-like tool that builds components. It supports tracking of dependencies among components, and a hierarchical reporting facility.
71
+ test_files: []
72
+