daedalus-core 0.0.1

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.
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ NTZjNzQ4ODQ3NDM2MDY4NGY2NjdmMjljYTFlMGY5YmQ3OTA4NDUzYg==
5
+ data.tar.gz: !binary |-
6
+ Y2E3YzA2Y2ZjZDU0ZGUyYmM4MzQzNGVkZGI0MjZhOTkyYmUxODgxMQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ ZDI2OGRkNTIxZGI1ZmExOGNhOTVmYjY5NzRlZjM2ZDdmZTk0NjFkZTUyZWVi
10
+ ZjgyNmE3YjAzOTc0YzUwNzRmYjgwNzkwYTZiZmI3ZTYxNGJjYjBjOWM5YmEx
11
+ N2MxM2VlMTc1YTliMTViMTNkMGE2NzY1NDMzNjJlMTU2NTY1MjQ=
12
+ data.tar.gz: !binary |-
13
+ MjdkYzU2NGQxNWQ4NDcxYjQzMjZmMWY2NTU2ZjI5ZDc4NDZiZWI0OWE1MDkx
14
+ NjNkZjgyMjkxNGIyY2FmNTcwYjk5NDA5MWM0ZGE4OTM2MmZjOTc5Mzk5MmZj
15
+ N2Q4OTc2NmFhYWU5YjViNDRhNmRjMTk1YzA0ODViZThiZjc1MTg=
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Brian Shirai. All rights reserved.
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ # Daedalus
2
+
3
+ Daedalus |ˈdedl-əs| Greek Mythology
4
+ a craftsman, considered the inventor of carpentry, who is said to have
5
+ built the labyrinth for Minos, king of Crete. Minos imprisoned him and
6
+ his son Icarus, but they escaped using wings that Daedalus made and
7
+ fastened with wax. Icarus, however, flew too near the sun and was killed.
8
+
9
+
10
+ In other words, he built things for a living.
11
+
12
+ Daedalus is an extraction from the Rubinius build system. Very few of the
13
+ features have been implemented at this point. It is very very alpha.
@@ -0,0 +1,28 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'daedalus/version'
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "daedalus-core"
6
+ gem.version = "#{Daedalus::VERSION}"
7
+ gem.authors = ["Brian Shirai"]
8
+ gem.email = ["brixen@gmail.com"]
9
+ gem.homepage = "https://github.com/rubinius/daedalus-core"
10
+
11
+ gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) unless File.extname(f) == ".bat" }.compact
12
+ gem.files = `git ls-files`.split("\n")
13
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
+ gem.require_paths = ["lib"]
15
+ gem.summary = <<-EOS
16
+ Daedalus is a build system extracted from Rubinius.
17
+ EOS
18
+ gem.has_rdoc = true
19
+ gem.extra_rdoc_files = %w[ README.md LICENSE ]
20
+ gem.rubygems_version = %q{1.3.5}
21
+
22
+ gem.rdoc_options << '--title' << 'Daedalus Gem' <<
23
+ '--main' << 'README' <<
24
+ '--line-numbers'
25
+
26
+ gem.add_development_dependency "rake", "~> 0.9"
27
+ gem.add_development_dependency "rspec", "~> 2.8"
28
+ end
@@ -0,0 +1,1016 @@
1
+ # -*- coding: utf-8 -*-
2
+ =begin
3
+
4
+ Daedalus |ˈdedl-əs| Greek Mythology
5
+ a craftsman, considered the inventor of carpentry, who is said to have
6
+ built the labyrinth for Minos, king of Crete. Minos imprisoned him and
7
+ his son Icarus, but they escaped using wings that Daedalus made and
8
+ fastened with wax. Icarus, however, flew too near the sun and was killed.
9
+
10
+
11
+ In other words, he built things for a living.
12
+
13
+ =end
14
+
15
+ require 'digest/sha1'
16
+ require 'thread'
17
+ require 'daedalus/dependency_grapher'
18
+
19
+ module Daedalus
20
+
21
+ class Logger
22
+ def initialize(level=3)
23
+ @count = 0
24
+ @total = nil
25
+ @level = level
26
+
27
+ @thread_count = 0
28
+ @count_mutex = Mutex.new
29
+ end
30
+
31
+ def thread_id
32
+ id = Thread.current[:build_id]
33
+ unless id
34
+ @count_mutex.synchronize do
35
+ @thread_count += 1
36
+ id = Thread.current[:build_id] = @thread_count
37
+ end
38
+ end
39
+
40
+ return id
41
+ end
42
+
43
+ def start(count)
44
+ @count = 0
45
+ @total = count
46
+ end
47
+
48
+ def stop
49
+ @total = nil
50
+ end
51
+
52
+ def inc!
53
+ @count += 1
54
+ end
55
+
56
+ def show(kind, cmd)
57
+ if @total
58
+ STDOUT.puts "[%3d/%3d] #{kind} #{cmd}" % [@count, @total]
59
+ else
60
+ STDOUT.puts "#{thread_id}: #{kind} #{cmd}"
61
+ end
62
+ end
63
+
64
+ def command(cmd)
65
+ system cmd
66
+ if $?.exitstatus != 0
67
+ STDOUT.puts "Error: #{cmd}"
68
+ raise "Error compiling"
69
+ end
70
+ end
71
+
72
+ def verbose(str)
73
+ if @level >= 5
74
+ STDOUT.puts "daedalus: #{str}"
75
+ end
76
+ end
77
+
78
+ def info(str)
79
+ if @level >= 3
80
+ STDOUT.puts "daedalus: #{str}"
81
+ end
82
+ end
83
+ end
84
+
85
+ class FancyLogger < Logger
86
+ HEADER = "Daedalus building:"
87
+
88
+ def initialize(level=0)
89
+ super 0
90
+ end
91
+
92
+ def start(count)
93
+ super
94
+ STDOUT.sync = true
95
+ STDOUT.puts HEADER
96
+ end
97
+
98
+ def show(kind, cmd)
99
+ @count += 1
100
+ perc = (100 * (@count.to_f / @total)).to_i
101
+ bar_size = (30 * (@count.to_f / @total)).to_i
102
+
103
+ bar = "#{'=' * bar_size}#{' ' * (30 - bar_size)}"
104
+
105
+ if cmd.size > 38
106
+ cmd = "..#{cmd[-38,38]}"
107
+ else
108
+ cmd = cmd.ljust(40)
109
+ end
110
+
111
+ STDOUT.print "\r[%3d%% #{bar}] #{kind} #{cmd}" % perc.to_i
112
+ end
113
+
114
+ def command(cmd)
115
+ output = IO.popen "sh -c '#{cmd} 2>&1'", "r"
116
+
117
+ begin
118
+ str = output.read
119
+ rescue Exception
120
+ Process.kill 'SIGINT', output.pid
121
+ STDOUT.puts "\nInterrupt compiling."
122
+ raise "Stopped compiling"
123
+ end
124
+
125
+ Process.wait output.pid
126
+
127
+ if $?.exitstatus != 0
128
+ STDOUT.puts "Error compiling: #{cmd}"
129
+ STDOUT.puts "Output:\n#{str}"
130
+ raise "Error compiling"
131
+ end
132
+ end
133
+
134
+ def stop
135
+ super
136
+ STDOUT.puts
137
+ end
138
+ end
139
+
140
+ class Compiler
141
+ def initialize(cc, cxx, ldshared, ldsharedxx, logger, blueprint)
142
+ @cc = cc
143
+ @cxx = cxx
144
+ @ldshared = ldshared
145
+ @ldsharedxx = ldsharedxx
146
+ @cflags = []
147
+ @cxxflags = []
148
+ @ldflags = []
149
+ @libraries = []
150
+ @log = logger
151
+ @blueprint = blueprint
152
+
153
+ @mod_times = Hash.new do |h,k|
154
+ h[k] = (File.exists?(k) ? File.mtime(k) : Time.at(0))
155
+ end
156
+
157
+ @sha1_mtimes = {}
158
+ @sha1s = Hash.new do |h,k|
159
+ if File.exists?(k)
160
+ @log.verbose "computing SHA1: #{k}"
161
+ @sha1_mtimes[k] = File.mtime(k)
162
+ h[k] = Digest::SHA1.file(k).hexdigest
163
+ else
164
+ h[k] = ""
165
+ end
166
+ end
167
+
168
+ @mtime_only = false
169
+ end
170
+
171
+ attr_accessor :mtime_only
172
+
173
+ def header_directories
174
+ dirs = []
175
+ @cflags.each do |fl|
176
+ fl.split(/\s+/).each do |part|
177
+ if part.index("-I") == 0
178
+ dirs << part[2..-1]
179
+ end
180
+ end
181
+ end
182
+ dirs
183
+ end
184
+
185
+ attr_reader :cc, :cxx, :ldshared, :ldsharedxx, :path, :cflags, :cxxflags, :ldflags, :log
186
+
187
+ def add_library(lib)
188
+ if f = lib.cflags
189
+ @cflags = f + @cflags
190
+ @cflags.uniq!
191
+ end
192
+
193
+ if f = lib.ldflags
194
+ @ldflags = f + @ldflags
195
+ @ldflags.uniq!
196
+ end
197
+ end
198
+
199
+ def mtime(path)
200
+ @mod_times[path]
201
+ end
202
+
203
+ def sha1(path)
204
+ if @sha1s.key?(path)
205
+ if File.mtime(path) > @sha1_mtimes[path]
206
+ @sha1s.delete(path)
207
+ end
208
+ end
209
+
210
+ @sha1s[path]
211
+ end
212
+
213
+ def compile(source, object)
214
+ if source =~ /\.cpp$/
215
+ cxx_compile(source, object)
216
+ else
217
+ c_compile(source, object)
218
+ end
219
+ end
220
+
221
+ def c_compile(source, object)
222
+ @log.show "CC", source
223
+ @log.command "#{@cc} #{@cflags.join(' ')} -c -o #{object} #{source}"
224
+ end
225
+
226
+ def cxx_compile(source, object)
227
+ @log.show "CXX", source
228
+ @log.command "#{@cxx} #{@cflags.join(' ')} #{@cxxflags.join(' ')} -c -o #{object} #{source}"
229
+ end
230
+
231
+ def link(path, files)
232
+ @log.show "LD", path
233
+ @log.command "#{@cxx} -o #{path} #{files.join(' ')} #{@libraries.join(' ')} #{@ldflags.join(' ')}"
234
+ end
235
+
236
+ def ar(library, objects)
237
+ @log.show "AR", library
238
+ @log.command "ar rv #{library} #{objects.join(' ')}"
239
+ @log.command "ranlib #{library}"
240
+ end
241
+
242
+ def link_shared(library, objects)
243
+ @log.show "LDSHARED", library
244
+ @log.command "#{@ldsharedxx} #{objects.join(' ')} -o #{library}"
245
+ end
246
+
247
+ def calculate_deps(path)
248
+ dirs = header_directories() + ["/usr/include"]
249
+ flags = @cflags.join(' ')
250
+ begin
251
+ dep = DependencyGrapher.new @cc, [path], dirs, flags
252
+ dep.process
253
+
254
+ # This is a quick work-around for a craptastic bug that I can't figure
255
+ # out. Sometimes this raises an exception saying it can't find a file
256
+ # which is pretty obviously there. I've been unable to figure out
257
+ # what causes this and thus how to fix.
258
+ #
259
+ # So as a temporary measure, if an exception is raised, I'm going to
260
+ # just do it again. Previous results have shown that this should
261
+ # work the 2nd time even though the first time failed.
262
+ #
263
+ # I know this sounds silly, but we need some fix for this.
264
+ rescue Exception
265
+ dep = DependencyGrapher.new @cc, [path], dirs, flags
266
+ dep.process
267
+ end
268
+
269
+ dep.sources.first.dependencies.sort
270
+ end
271
+ end
272
+
273
+ class Path
274
+ def initialize(path)
275
+ @path = path
276
+
277
+ if File.exists?(data_path)
278
+ begin
279
+ File.open data_path, "rb" do |f|
280
+ @data = Marshal.load(f.read)
281
+ end
282
+ rescue
283
+ STDERR.puts "WARNING: Path#initialize: load '#{data_path}' failed"
284
+ @data = {}
285
+ end
286
+ else
287
+ @data = {}
288
+ end
289
+ end
290
+
291
+ attr_reader :data, :path
292
+
293
+ def basename
294
+ File.basename @path
295
+ end
296
+
297
+ def artifacts_path
298
+ dir = File.join File.dirname(@path), "artifacts"
299
+ Dir.mkdir dir unless File.directory?(dir)
300
+ return dir
301
+ end
302
+
303
+ def data_path
304
+ File.join artifacts_path, "#{basename}.data"
305
+ end
306
+
307
+ def save!
308
+ File.open(data_path, "wb") do |f|
309
+ f << Marshal.dump(data)
310
+ end
311
+ end
312
+ end
313
+
314
+ class SourceFile < Path
315
+
316
+ def initialize(path)
317
+ super
318
+ @static_deps = []
319
+ @autogen_builder = nil
320
+ end
321
+
322
+ def depends_on(static_deps)
323
+ @static_deps = static_deps
324
+ end
325
+
326
+ def autogenerate(&builder)
327
+ @autogen_builder = builder
328
+ end
329
+
330
+ def object_path
331
+ File.join artifacts_path, "#{basename}.o"
332
+ end
333
+
334
+ def dependencies(ctx)
335
+ deps = @data[:deps]
336
+
337
+ if ctx.sha1(@path) != @data[:dep_sha1] or !deps
338
+ deps = recalc_depedencies(ctx)
339
+ end
340
+
341
+ return deps + @static_deps
342
+ end
343
+
344
+ def recalc_depedencies(ctx)
345
+ deps = ctx.calculate_deps(@path)
346
+
347
+ @data[:dep_sha1] = ctx.sha1(@path)
348
+ @data[:deps] = deps
349
+
350
+ return deps
351
+ end
352
+
353
+ def sha1(ctx)
354
+ sha1 = Digest::SHA1.new
355
+ sha1 << ctx.sha1(@path)
356
+
357
+ begin
358
+ dependencies(ctx).each do |d|
359
+ sha1 << ctx.sha1(d)
360
+ end
361
+ rescue StandardError
362
+ recalc_depedencies(ctx)
363
+
364
+ sha1 = Digest::SHA1.new
365
+ sha1 << ctx.sha1(@path)
366
+
367
+ dependencies(ctx).each do |d|
368
+ begin
369
+ sha1 << ctx.sha1(d)
370
+ rescue StandardError
371
+ raise "Unable to find dependency '#{d}' from #{@path}"
372
+ end
373
+ end
374
+ end
375
+
376
+ sha1.hexdigest
377
+ end
378
+
379
+ def newer_dependencies(ctx)
380
+ dependencies(ctx).find_all do |x|
381
+ ctx.mtime(x) > ctx.mtime(object_path)
382
+ end
383
+ end
384
+
385
+ def out_of_date?(ctx)
386
+ unless File.exists?(@path)
387
+ return true if @autogen_builder
388
+ raise Errno::ENOENT, "Missing #{@path}"
389
+ end
390
+
391
+ return true unless File.exists?(object_path)
392
+
393
+ return true if ctx.mtime_only and ctx.mtime(@path) > ctx.mtime(object_path)
394
+
395
+ return true unless @data[:sha1] == sha1(ctx)
396
+ return false
397
+ end
398
+
399
+ def consider(ctx, tasks)
400
+ tasks << self if out_of_date?(ctx)
401
+ end
402
+
403
+ def build(ctx)
404
+ ctx.log.inc!
405
+
406
+ if @autogen_builder
407
+ ctx.log.show "GN", @path
408
+ @autogen_builder.call(ctx.log)
409
+ end
410
+
411
+ @data[:sha1] = sha1(ctx)
412
+ ctx.compile path, object_path
413
+ save!
414
+ end
415
+
416
+ def clean
417
+ File.unlink object_path if File.exists?(object_path)
418
+ File.unlink data_path if File.exists?(data_path)
419
+
420
+ Dir.rmdir artifacts_path if Dir.entries(artifacts_path).empty?
421
+ end
422
+
423
+ def describe(ctx)
424
+ if !File.exists?(object_path)
425
+ puts "#{@path}: unbuilt"
426
+ else
427
+ if @data[:sha1] != sha1(ctx)
428
+ puts "#{@path}: out-of-date"
429
+ end
430
+
431
+ deps = newer_dependencies(ctx)
432
+
433
+ unless deps.empty?
434
+ puts "#{@path}: dependencies changed"
435
+ deps.each do |d|
436
+ puts " - #{d}"
437
+ end
438
+ end
439
+ end
440
+ end
441
+
442
+ def info(ctx)
443
+ puts @path
444
+ puts " object: #{object_path}"
445
+ puts " last hash: #{@data[:sha1]}"
446
+ puts " curr hash: #{sha1(ctx)}"
447
+
448
+ puts " dependencies:"
449
+ dependencies(ctx).each do |x|
450
+ puts " #{x}"
451
+ end
452
+ end
453
+ end
454
+
455
+ class ExternalLibrary
456
+ def initialize(path)
457
+ @path = path
458
+
459
+ @cflags = nil
460
+ @ldflags = nil
461
+ @objects = nil
462
+
463
+ @build_dir = path
464
+ @builder = nil
465
+ @data = nil
466
+ end
467
+
468
+ attr_accessor :path, :cflags, :ldflags, :objects
469
+
470
+ def to_build(&blk)
471
+ @builder = blk
472
+
473
+ @data_file = "#{@build_dir}.data"
474
+
475
+ if File.exists?(@data_file)
476
+ begin
477
+ File.open @data_file, "rb" do |f|
478
+ @data = Marshal.load(f.read)
479
+ end
480
+ rescue
481
+ STDERR.puts "WARNING: ExternalLibrary#to_build: load '#{data_path}' failed"
482
+ @data = {}
483
+ end
484
+ else
485
+ @data = {}
486
+ end
487
+
488
+ end
489
+
490
+ def file(f)
491
+ File.join(@path, f)
492
+ end
493
+
494
+ def sha1
495
+ sha1 = Digest::SHA1.new
496
+
497
+ Dir["#{@build_dir}/*"].each do |f|
498
+ sha1.file(f) if File.file?(f)
499
+ end
500
+
501
+ Dir["#{@build_dir}/**/*"].each do |f|
502
+ sha1.file(f) if File.file?(f)
503
+ end
504
+
505
+ sha1.hexdigest
506
+ end
507
+
508
+ def have_objects
509
+ return true unless @objects
510
+ @objects.each do |o|
511
+ return false unless File.exists?(o)
512
+ end
513
+
514
+ return true
515
+ end
516
+
517
+ def out_of_date?(ctx)
518
+ return true unless have_objects
519
+ return false unless @builder
520
+ return false if @data and @data[:sha1] == sha1
521
+ return true
522
+ end
523
+
524
+ def consider(ctx, tasks)
525
+ tasks.pre << self if out_of_date?(ctx)
526
+ end
527
+
528
+ def build(ctx)
529
+ raise "Unable to build" unless @builder
530
+
531
+ ctx.log.inc!
532
+
533
+ ctx.log.show "LB", @build_dir
534
+
535
+ Dir.chdir(@build_dir) do
536
+ @builder.call(ctx.log)
537
+ end
538
+
539
+ @data[:sha1] = sha1()
540
+
541
+ File.open(@data_file, "wb") do |f|
542
+ f << Marshal.dump(@data)
543
+ end
544
+ end
545
+
546
+ def describe(ctx)
547
+ end
548
+ end
549
+
550
+ class Library
551
+ attr_reader :sources
552
+
553
+ def initialize(path, base, compiler)
554
+ @base = base
555
+ @compiler = compiler
556
+
557
+ @directory = File.dirname path
558
+ @library = File.basename path
559
+ @sources = []
560
+
561
+ yield self if block_given?
562
+
563
+ source_files "#{path}.c" if @sources.empty?
564
+ end
565
+
566
+ def path
567
+ File.join @base, @directory, library
568
+ end
569
+
570
+ def name
571
+ File.join @directory, library
572
+ end
573
+
574
+ def source_files(*patterns)
575
+ Dir.chdir @base do
576
+ patterns.each do |t|
577
+ Dir[t].each do |f|
578
+ @sources << SourceFile.new(f)
579
+ end
580
+ end
581
+ end
582
+ end
583
+
584
+ def object_files
585
+ @sources.map { |s| s.object_path }
586
+ end
587
+
588
+ def out_of_date?(compiler)
589
+ Dir.chdir @base do
590
+ return true unless File.exists? name
591
+ @sources.each do |s|
592
+ return true unless File.exists? s.object_path
593
+ return true if File.mtime(s.object_path) > File.mtime(name)
594
+ end
595
+ @sources.any? { |s| s.out_of_date? compiler }
596
+ end
597
+ end
598
+
599
+ def consider(compiler, tasks)
600
+ tasks.pre << self if out_of_date? compiler
601
+ end
602
+
603
+ def clean
604
+ Dir.chdir @base do
605
+ @sources.each { |s| s.clean }
606
+ File.delete name if File.exists? name
607
+ end
608
+ end
609
+ end
610
+
611
+ class StaticLibrary < Library
612
+ def library
613
+ "#{@library}.a"
614
+ end
615
+
616
+ def build(compiler)
617
+ Dir.chdir @base do
618
+ # TODO: out of date checking should be subsumed in building
619
+ @sources.each { |s| s.build @compiler if s.out_of_date? @compiler }
620
+ @compiler.ar name, object_files
621
+ end
622
+ end
623
+ end
624
+
625
+ class SharedLibrary < Library
626
+ def library
627
+ "#{@library}.#{RbConfig::CONFIG["DLEXT"]}"
628
+ end
629
+
630
+ def build(compiler)
631
+ Dir.chdir @base do
632
+ # TODO: out of date checking should be subsumed in building
633
+ @sources.each { |s| s.build @compiler if s.out_of_date? @compiler }
634
+ @compiler.link_shared name, object_files
635
+ end
636
+ end
637
+ end
638
+
639
+ # The purpose of a LibraryGroup is to combine multiple static and shared
640
+ # libraries into a unit. Static libraries are used to statically link a
641
+ # program, while shared libraries may be dynamically loaded by that program
642
+ # or another program.
643
+ #
644
+ # NOTE: The current protocol for getting a list of static libraries is the
645
+ # #objects method. This should be changed when reworking Daedalus.
646
+ class LibraryGroup
647
+ attr_accessor :cflags, :ldflags
648
+
649
+ def initialize(base, compiler)
650
+ @base = base
651
+ @static_libraries = []
652
+ @shared_libraries = []
653
+ @compiler = Compiler.new(compiler.cc,
654
+ compiler.cxx,
655
+ compiler.ldshared,
656
+ compiler.ldsharedxx,
657
+ compiler.log, nil)
658
+
659
+ yield self
660
+
661
+ compiler.add_library self
662
+
663
+ @compiler.cflags.concat cflags if cflags
664
+ @compiler.ldflags.concat ldflags if ldflags
665
+ end
666
+
667
+ def depends_on(file, command)
668
+ # TODO: HACK, the agony, this should be implicit
669
+ unless File.exists? File.join(@base, file)
670
+ raise "library group #{@base} depends on #{file}, please run #{command}"
671
+ end
672
+ end
673
+
674
+ # TODO: change the way files are sorted
675
+ def path
676
+ @base
677
+ end
678
+
679
+ def static_library(path, &block)
680
+ @static_libraries << StaticLibrary.new(path, @base, @compiler, &block)
681
+ end
682
+
683
+ def shared_library(path, &block)
684
+ @shared_libraries << SharedLibrary.new(path, @base, @compiler, &block)
685
+ end
686
+
687
+ # TODO: Fix this protocol
688
+ def objects
689
+ @static_libraries.map { |l| l.path }
690
+ end
691
+
692
+ def libraries
693
+ @static_libraries + @shared_libraries
694
+ end
695
+
696
+ def consider(compiler, tasks)
697
+ # TODO: Note we are using @compiler, not compiler. There should not be a
698
+ # global compiler. There should be a global configuration object that is
699
+ # specialized by specific libraries as needed.
700
+ libraries.each { |l| l.consider @compiler, tasks }
701
+ end
702
+
703
+ def clean
704
+ libraries.each { |l| l.clean }
705
+ end
706
+ end
707
+
708
+ class Program < Path
709
+ def initialize(path, files)
710
+ super path
711
+ @files = files.sort_by { |x| x.path }
712
+ end
713
+
714
+ def objects
715
+ # This partitions the list into .o's first and .a's last. This
716
+ # is because gcc on some platforms require that static libraries
717
+ # be linked last. This is because gcc builds a list of undefined
718
+ # symbols, and then when it hits a .a, looks in the archive
719
+ # to try and resolve those symbols. So if a .o needs something
720
+ # from a .a, the .a MUST come after the .o
721
+ objects = []
722
+ archives = []
723
+
724
+ @files.each do |x|
725
+ if x.respond_to? :object_path
726
+ if File.extname(x.object_path) == ".a"
727
+ archives << x.object_path
728
+ else
729
+ objects << x.object_path
730
+ end
731
+ else
732
+ x.objects.each do |obj|
733
+ if File.extname(obj) == ".a"
734
+ archives << obj
735
+ else
736
+ objects << obj
737
+ end
738
+ end
739
+ end
740
+ end
741
+
742
+ objects.sort + archives
743
+ end
744
+
745
+ def consider(ctx, tasks)
746
+ @files.each { |x| x.consider(ctx, tasks) }
747
+ tasks.post << self unless tasks.empty? and File.exists?(@path)
748
+ end
749
+
750
+ def build(ctx)
751
+ ctx.log.inc!
752
+ ctx.link @path, objects
753
+ end
754
+
755
+ def clean
756
+ @files.each do |f|
757
+ f.clean if f.respond_to? :clean
758
+ end
759
+
760
+ File.unlink @path if File.exists?(@path)
761
+ File.unlink data_path if File.exists?(data_path)
762
+ Dir.rmdir artifacts_path if Dir.entries(artifacts_path).empty?
763
+ end
764
+
765
+ def describe(ctx)
766
+ puts "Program: #{@path}"
767
+
768
+ @files.each do |f|
769
+ f.describe(ctx)
770
+ end
771
+ end
772
+
773
+ def file_info(ctx, files)
774
+ files.each do |n|
775
+ obj = @files.find { |x| x.path == n }
776
+ if obj
777
+ obj.info(ctx)
778
+ else
779
+ puts "Unable to find file: #{n}"
780
+ end
781
+ end
782
+ end
783
+ end
784
+
785
+ class Tasks
786
+ def initialize
787
+ @pre = []
788
+ @default = []
789
+ @post = []
790
+ end
791
+
792
+ attr_reader :pre, :default, :post
793
+
794
+ def <<(obj)
795
+ @default << obj
796
+ end
797
+
798
+ def empty?
799
+ @pre.empty? and @default.empty? and @post.empty?
800
+ end
801
+ end
802
+
803
+ class TaskRunner
804
+ def initialize(compiler, tasks, max=nil)
805
+ @max = TaskRunner.detect_cpus
806
+ @tasks = tasks
807
+ @compiler = compiler
808
+
809
+ calculate_max(max)
810
+ end
811
+
812
+ def calculate_max(max)
813
+ cpus = TaskRunner.detect_cpus
814
+
815
+ case max
816
+ when nil # auto
817
+ case cpus
818
+ when 1, 2
819
+ @max = cpus
820
+ when 4
821
+ @max = 3
822
+ else
823
+ @max = 4
824
+ end
825
+ when Fixnum
826
+ @max = max
827
+ when "cpu"
828
+ @max = cpus
829
+ end
830
+ end
831
+
832
+ def self.detect_cpus
833
+ if RUBY_PLATFORM =~ /windows/
834
+ return 1
835
+ else
836
+ if RUBY_PLATFORM =~ /bsd/
837
+ key = 'NPROCESSORS_CONF'
838
+ else
839
+ key = '_NPROCESSORS_CONF'
840
+ end
841
+ count = `getconf #{key} 2>&1`.to_i
842
+ return 1 if $?.exitstatus != 0
843
+ return count
844
+ end
845
+ end
846
+
847
+ def start
848
+ linear_tasks @tasks.pre
849
+ perform_tasks @tasks.default
850
+ linear_tasks @tasks.post
851
+ end
852
+
853
+ def linear_tasks(tasks)
854
+ tasks.each do |task|
855
+ task.build @compiler
856
+ end
857
+ end
858
+
859
+ def perform_tasks(tasks)
860
+ count = tasks.size
861
+
862
+ puts "Running #{count} tasks using #{@max} parallel threads"
863
+ start = Time.now
864
+
865
+ queue = Queue.new
866
+ threads = []
867
+
868
+ @max.times do
869
+ threads << Thread.new {
870
+ while true
871
+ task = queue.pop
872
+ break unless task
873
+ task.build @compiler
874
+ end
875
+ }
876
+ end
877
+
878
+ sync = []
879
+
880
+ queue_tasks(queue, tasks, sync)
881
+
882
+ # Kill off the builders
883
+ threads.each do |t|
884
+ queue << nil
885
+ end
886
+
887
+ threads.each do |t|
888
+ t.join
889
+ end
890
+
891
+ sync.each do |task|
892
+ task.build @compiler
893
+ end
894
+
895
+ puts "Build time: #{Time.now - start} seconds"
896
+ end
897
+
898
+ def queue_tasks(queue, tasks, sync)
899
+ tasks.each do |task|
900
+ if task.kind_of? Array
901
+ queue_tasks queue, task[1..-1], sync
902
+ sync << task[0]
903
+ else
904
+ queue.push task
905
+ end
906
+ end
907
+ end
908
+ end
909
+
910
+ class Blueprint
911
+ def initialize
912
+ @programs = []
913
+ @compiler = nil
914
+ end
915
+
916
+ def external_lib(path)
917
+ ex = ExternalLibrary.new(path)
918
+ yield ex
919
+ ex
920
+ end
921
+
922
+ def library_group(path, &block)
923
+ LibraryGroup.new(path, @compiler, &block)
924
+ end
925
+
926
+ def gcc!(cc, cxx, ldshared, ldsharedxx)
927
+ @compiler = Compiler.new(cc,
928
+ cxx,
929
+ ldshared,
930
+ ldsharedxx,
931
+ Logger.new, self)
932
+ end
933
+
934
+ def source_files(*patterns)
935
+ files = []
936
+
937
+ patterns.each do |t|
938
+ Dir[t].each do |f|
939
+ files << SourceFile.new(f)
940
+ end
941
+ end
942
+
943
+ files
944
+ end
945
+
946
+ def source_file(file)
947
+ sf = SourceFile.new(file)
948
+ yield sf if block_given?
949
+ sf
950
+ end
951
+
952
+ def program(name, *files)
953
+ @programs << Program.new(name, files)
954
+ end
955
+
956
+ def build(targets=[], jobs=nil)
957
+ if !targets.empty?
958
+ @programs.each do |x|
959
+ if targets.include? x.path
960
+ tasks = Tasks.new
961
+ x.consider @compiler, tasks
962
+
963
+ if tasks.empty?
964
+ @compiler.log.info "Nothing to do for #{x.path}"
965
+ else
966
+ tr = TaskRunner.new @compiler, tasks, jobs
967
+ tr.start
968
+ end
969
+ end
970
+ end
971
+ else
972
+ @programs.each { |x| x.build @compiler }
973
+ end
974
+ end
975
+
976
+ def clean(targets=[])
977
+ if !targets.empty?
978
+ @programs.each do |x|
979
+ if targets.include? x.path
980
+ x.clean
981
+ end
982
+ end
983
+ else
984
+ @programs.each { |x| x.clean }
985
+ end
986
+ end
987
+
988
+ def describe(targets=[])
989
+ if !targets.empty?
990
+ @programs.each do |x|
991
+ if targets.include? x.path
992
+ x.describe @compiler
993
+ end
994
+ end
995
+ else
996
+ @programs.each { |x| x.describe @compiler }
997
+ end
998
+ end
999
+
1000
+ def file_info(files)
1001
+ @programs.each do |x|
1002
+ x.file_info @compiler, files
1003
+ end
1004
+ end
1005
+ end
1006
+
1007
+ def self.blueprint
1008
+ b = Blueprint.new
1009
+ yield b
1010
+ b
1011
+ end
1012
+
1013
+ def self.load(file)
1014
+ eval File.read(file)
1015
+ end
1016
+ end