daedalus-core 0.0.1

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