-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtargets.rb
More file actions
2025 lines (1747 loc) · 76.5 KB
/
targets.rb
File metadata and controls
2025 lines (1747 loc) · 76.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/ruby -w
# Ruby-based build system
# Author: ram (Munagala V. Ramanath)
#
# Classes for targets of various types
#
%w{ date find thread open3 set }.each { |f| require f }
# Classes associated with various targets
class Build # main class and namespace
# Bare minimal interface for a target: we assume that a target is embodied by a single
# file. In the typical case this will be an object file generated by the assembler,
# compiler or linker; in the atypical case (building firefox for example), this will be
# a file generated by a Perl or Python script. The path to this file and its timestamp
# are recorded here. Each target also needs the following methods:
# out_of_date? -- return true iff target is out of date
# get_cmd -- get the command to build the target
# make -- run the command to build the target
#
module Target
# path -- absolute path of target file
# mtime -- modification time of target file
# build -- current build object (needed for compiler options, etc.)
# ood -- true iff target is known to be out-of-date
#
# deps -- dependencies: array of target objects
# visit -- transient variable that exists only for the duration of a Depth First
# Search; values: :start, :finish, nil
# needed_by -- transient variable: set of targets that need this one to be up-to-date
# before they can be built
# enqueued -- true iff target was enqueued for building
# rebuilt -- true iff this target was rebuilt (undefined otherwise)
#
attr :path, :mtime, :build, :ood
attr_accessor :deps, :visit, :needed_by, :enqueued, :rebuilt
# p -- full path to target
# b -- build object
# d -- dependency list
#
def initialize p, b, d = []
raise "build is nil" if !b
@path = Util.strip p
d = [] if !d # user may pass nil
@build, @deps = b, d
# cannot initialize @mtime since file may not yet exist
#Build.logger.debug "Initialized %s, deps.size = %d" % [p, d.size]
end # initialize
# Performs these steps (invoked from a separate thread in the thread pool):
# 1. Build this object file and save mtime
# 2. Update statistics with build times.
# 3. Set @rebuilt to true so we can identify targets that were actually built.
# 4. Update t.needs sets for all targets t in @needed_by
#
def make
log = Build.logger
# if this target is being built, some other target should need it or this should be
# a top-level target; in both cases @needed_by should be defined
#
raise "needed_by not defined for %s" % @path if !defined? @needed_by
# assume all checks have been made earlier and build this target
cmd = get_cmd
t = Time.new
Util.run_cmd cmd, Build.logger
t = Time.new - t
update_stats t
@rebuilt = true # for persistence
File.check_er @path
@mtime = File::stat( @path ).mtime
@ood = false
# update targets that depend on this one
if :top == @needed_by # this is a top-level target
@build.lock.synchronize {
status = @build.needs.delete? self
raise "Failed to delete %s from build.needs" % @path if status.nil?
# release main thread if no out-of-date top-level targets remain
@build.done.signal if @build.needs.empty?
}
return
end
raise "Unexpected: @needed_by is empty for %s" % @path if @needed_by.empty?
# this target is needed by one or more other targets
@needed_by.each { |tgt|
ready = false
tgt.lock.synchronize {
#log.debug "tgt.needs.size = %d before deleting %s from %s" %
# [tgt.needs.size, @path, tgt.path]
status = tgt.needs.delete? self
raise "Failed to delete %s from tgt.needs (%s)" [@path, tgt.path] if
status.nil?
ready = tgt.needs.empty? # whether tgt is ready to be enqueued
}
# NOTE: doing this inside the synchronize block causes strange behavior
next if !ready
# enqueue tgt since all of its dependencies are current
raise "Unexpected: %s already enqueued" % tgt.path if tgt.enqueued
build.thr_pool.add Task.new( tgt, :make )
log.debug "Added %s to task queue" % tgt.path
tgt.enqueued = true
}
end # make
def read_mtime # get mtime if file is present
return nil if !File.exist? @path
@mtime = File::stat( @path ).mtime if !defined? @mtime
return @mtime
end # read_mtime
def remove_visit # remove transient instance variable (unused)
remove_instance_variable :@visit
end # remove_visit
end # Target
# .h, .c or other source file; note that this is a target representing the source file
# itself, not the object file generated from that source file
#
class SourceFileTarget
include Target
# .hpp, tcc and ipp occur in boost:
# /usr/include/c++/4.4/bits/basic_string.tcc
# /usr/include/boost/date_time/gregorian_calendar.ipp
#
R_SRC_EXT = /\.(?:h|hpp|ipp|tcc|cc|cpp|c|C|S|s)$/o # supported source file extensions
# cmd
# if defined, this is a simple shell command or lambda to generate this target; not
# intended for heavy duty generators like lex/yacc which should use the same
# mechanism as C/C++.
#
attr :cmd
# path : path to source
# build : current build
# cmd : generation command if any
#
def initialize a_path, a_build, a_cmd = nil
p = Util.strip a_path
ext = File.extname p
# we allow empty extensions because we have system dependencies like this:
# /usr/include/c++/4.4/cstddef
#
Build.logger.warn "Unexpected extension in #{p}" if !ext.empty? && ext !~ R_SRC_EXT
super p, a_build
if a_cmd # could be string (shell command) or lambda
@cmd = a_cmd.is_a?( String ) ? Util.strip( a_cmd ) : a_cmd
return
end
# no generation command, so check that the file exists and get mtime
File.check_er @path
@mtime = File::stat( @path ).mtime
end # initialize
def make # build this source target if we have a command
# revise later when have a DB to store cmd as well; that way, we can rebuild if file
# exists but cmd has changed.
#
if !File.exist? @path # run generation command
raise "No command to make %s" % path if !defined? @cmd
if @cmd.is_a?( String )
Util.run_cmd @cmd, Build.logger
File.check_er @path
else # must be a lambda
@cmd.call
end
@mtime = File::stat( @path ).mtime
else
@mtime = File::stat( @path ).mtime if !defined?( @mtime )
end
end # make
def bname # return basename with extension stripped
File.bname @path
end # bname
def extname # return extension name
File.extname @path
end # extname
end # SourceFileTarget
class CompileOrLinkTarget # base class for Compilable and Linkable
include Target
# options -- custom compiler, assembler or linker options (if any) for this target
# build_type
# :dbg, :opt, :rel (debug, optimized, release); we can further distinguish objects
# intended for dynamic or static linking (i.e. whether they were compiled with
# "-fPIC -DPIC" or not) if necessary later with a @dynamic boolean
#
attr :options, :build_type
# get options of given type -- local set if it exists, global set otherwise. When the
# global set is returned, a copy is made if the second argument is true.
# When we need to make target-specific changes to options, we get a copy of the global
# options, make changes to the copy and set it as the option list for this target.
# When we need to construct the actual command to rebuild the target, we won't be
# making a copy so copy = false in this case.
#
def get_options type, copy = true
return @options if defined? @options
opt = build.options.options[ type ]
return copy ? opt.dup : opt
end # get_options
end # CompileOrLinkTarget
# object file obtained by compiling or assembling a source (.{s|S|c|C}) file
class Compilable < CompileOrLinkTarget
# options_cpp -- custom pre-processor options (if any) for this target
# no_hdr_deps
# true iff this object file has no header file dependencies (so deps will have
# only one item: the source file); this is a rare case but could happen if the
# source file has no pre-processor #include directives. If this variable is
# defined, the value must be true.
#
attr :options_cpp
attr_accessor :no_hdr_deps
# map file extension to object type
EXT2TYPE = { '.o_dbg' => :dbg, '.o_opt' => :opt, '.o_rel' => :rel }
# a_src -- source file target (object)
# a_path -- path to object file
# a_build -- current build object
#
def initialize a_src, a_path, a_build
raise "SourceFileTarget != #{a_src.class.name}" if a_src.class != SourceFileTarget
# sanity check: base names of source and object should be the same
bname = File.bname a_path
raise "Base names differ: #{bname} != #{a_src.bname}" if bname != a_src.bname
super a_path, a_build
# set the build_type
ext = File.extname a_path
@build_type = EXT2TYPE[ ext ]
raise "Bad extension: '#{ext}'" if !@build_type
raise "Build type mismatch: #{@build_type} != #{@build.build_type}" if
@build_type != @build.build_type
@deps = [a_src] # first dependency should always be the source file target
# object file may not yet exist, so cannot stat it
end # initialize
# get cpp options for this target -- local set if present, otherwise global set. If
# arg is true, a copy of the global set is returned; used when target-specific
# modifications are made to the option set; arg is false when the options are used
# for generating the compile command since making a copy is unnecessary then.
#
def get_cpp_options copy = true
return @options_cpp if defined? @options_cpp
opt = @build.options.options[ :cpp ]
return copy ? opt.dup : opt
end # get_cpp_options
# return true iff this target is out-of-date and needs to be made; since this is a
# single object file generated by the compiler or assembler, neither self.needs nor
# d.needed_by for dependencies d (which will be source or header files) is useful.
#
def out_of_date?
return @ood if defined? @ood
log = Build.logger
# check if timestamp on source is more recent (first dependency is always source)
src = @deps[ 0 ]
src.make # generate source file if necessary and possible
raise "Source %s has no timestamp" % src.path if !src.mtime
if !File.exist?( @path ) # easy case -- file missing
log.info "%s out of date since file is absent" % @path
@ood = true
return @ood
end
# file exists
@mtime = File::stat( @path ).mtime if ! defined? @mtime
if 1 == @deps.size # special case when source file is the only dependency
raise "No header file deps for #{@path}; was discover_deps run ?" if
!defined? @no_hdr_deps
@ood = @mtime < src.mtime
if @ood
log.info "%s out of date since %s is newer (%s < %s)" %
[@path, src.path, @mtime, src.mtime]
else
msg = ("%s is current since %s is the only dependency and is older" +
" (%s >= %s)") % [@path, src.path, @mtime, src.mtime]
log.info msg
end
return @ood
end
# check mtime of all dependencies (first dependency is the source file; the rest
# are header files)
#
@deps.each { |d|
# We expect all source and header files to exist; if this is not the case (e.g.
# some of them are generated, we'll need to revise this code
#
raise "Dependency %s of %s has no mtime" % [d.path, @path] if d.mtime.nil?
next if @mtime >= d.mtime # dependency is older
log.info "%s out of date since dependency %s is newer (%s < %s)" %
[@path, d.path, @mtime, d.mtime]
@ood = true
return @ood
}
# all dependencies are older
log.info "%s is current since all dependencies are older" % @path
@ood = false
return @ood
end # out_of_date?
# remove options from this target
#
# args is a hash with these keys:
# options -- array of options to delete [required]
# type -- type of options (:cpp) [optional]; it is
# required when deleting pre-processor options from assembler, C and C++
# targets and should be :cpp in those cases; in other cases, it is
# deduced from target type and should be omitted.
# err -- if false, no exception is raised if option is not found; default is
# true [optional]
#
# sample usage:
# delete_options( :options => ['-Wl,-rpath', '-Wl,/opt/foo/lib'],
# :option_type => :ld_cc_lib,
# :err => false )
#
def delete_options args
opt = args[ :options ]
raise "Missing options" if !opt
raise "Empty options" if opt.empty?
type, err = args[ :option_type ], args[ :err ]
err = true if err.nil?
# make a local copy of global options if necessary and delete options from copy
if type
raise "Bad type: #{type}" if :cpp != type
@options_cpp = get_cpp_options
@options_cpp.delete opt, err
else
@options = get_options
@options.delete opt, err
end
Build.logger.debug "Deleted %s from target %s (type = %s)" % [opt.to_s, @path, type]
end # delete_options
# add options to this target
#
# args is a hash with these keys:
# options -- array of options to add [required]
# type -- type of options (:cpp) [optional];
# required only when adding pre-processor options to assembler, C and C++
# targets and should be :cpp in those cases; in other cases, it is
# deduced from target type and should be omitted.
# replace -- if false, an exception is raised if an existing option conflicts with
# new options; if true, such conflicting options are removed; default is
# false [optional]
#
# sample usage:
# add_options( :options => ['-Wtype-limits'],
# :option_type => :cpp,
# :replace => true )
#
def add_options args
opt = args[ :options ]
raise "Missing options" if !opt
raise "Empty options" if opt.empty?
type, err = args[ :option_type ], args[ :err ]
err = true if err.nil?
# make a local copy of global options if necessary and add options to copy
if type
raise "Bad type: #{type}" if :cpp != type
@options_cpp = get_cpp_options
@options_cpp.add opt, err
else
@options = get_options
@options.add opt, err
end
Build.logger.debug "Added %s to target %s (type = %s)" % [opt.to_s, @path, type]
end # add_options
end # Compilable
# object file from assembler source
class ObjFileTargetAS < Compilable
def initialize a_src, a_path, a_build
ext = a_src.extname
raise "Empty src extension" if ext.empty?
raise "Bad source file extension in: #{ext}" if ext !~ /.[sS]/o
super a_src, a_path, a_build
end # initialize
# get options of appropriate type for this target (local set if it exists, a copy of
# global set otherwise)
#
def get_options copy = true
super :as, copy
end # get_options
def get_cmd # return command to rebuild this object
# We currently do not support C pre-processor directives in assembler files; if
# and when we do, we'll need to add CPP options like this:
# cpp_opt = get_cpp_options false
# Then add before as_opt.to_s in the array: cpp_opt.to_s
#
as_opt = get_options false
return [@build.cc, "-o %s -c %s" % [@path, @deps[ 0 ].path], as_opt.to_s].join ' '
end # get_cmd
def update_stats time # elapsed time
@build.stats.update :@num_as_objs, time
Util.print_status @build.thr_pool.tasks.size # update status message on screen
end # update_stats
end # ObjFileTargetAS
class ObjFileTargetCLike < Compilable # object file from C-like source (C, C++, ...)
R_C_EXT = /\A\.(?:c)\Z/o # C file extensions
R_CXX_EXT = /\A\.(?:C|cc|cpp)\Z/o # C++ file extensions
# a_src -- target object for source file
# a_path -- path to object file
# a_build -- current build object
# src_ext -- source file extension (used from ObjFileTargetCXX)
#
def initialize a_src, a_path, a_build, src_ext = /.c/o
ext = a_src.extname
raise "Unexpected: src_ext is nil" if !src_ext
raise "Bad source file extension: #{ext}" if ext !~ src_ext
super a_src, a_path, a_build
# We might create all 3 targets (.o_dbg, .o_opt, .o_rel) for each source file found
# because it allows us to perform more than one type of build during the course of a
# single run; so skip this check:
# raise "Build type mismatch: #{@build_type} != #{@build.build_type}" if
# @build_type != @build.build_type
end # initialize
# Invoked _only_ for C and C++ object files.
# return true if dependency list from persistence db is usable, false otherwise
# if returning true, should set @hdrs to array of header file dependencies (or set
# @no_hdr_deps if there no header files)
#
def set_deps_from_db compiler, cpp_opt
db = @build.db
return false if ! db # persistence not enabled
t = db.get_obj @path
return false if !t # no persisted data
log = Build.logger
if !File.exist?( @path )
# it may still be the case that the dependency list from the DB is usable but
# to verify that we need timestamps of all dependencies from the previous build
# which, currently, are not saved to the database. If we had them and they are
# unchanged we can return true (but still set @ood to true since the object
# file needs to be generated)
#
log.info "%s out-of-date since file is absent" % @path
@ood = true
return false
end
# object file exists; get timestamp and compare to that of source
@mtime = File::stat( @path ).mtime if !defined?( @mtime )
sf = @deps.first # first dependency is the source file
if @mtime <= sf.mtime
log.info "%s out-of-date since source file %s is newer" % [@path, sf.path]
@ood = true
return false
end
saved_compiler = db.get( '.c' =~ get_src_ext ? Build::KEY_CC_PATH
: Build::KEY_CXX_PATH )
if compiler != saved_compiler
# different compilers have different pre-defined pre-processor symbols so it is
# possible that the set of header file dependencies is different
#
log.info "%s out-of-date since compiler is different: %s != %s" %
[@path, compiler, saved_compiler]
@ood = true
return false
end
# needs fixing: compare CPP options, not compile options.
# Get saved custom CPP options for this object, if any; otherwise, global CPP
# options
#
saved_opt = t.options_cpp
saved_opt = db.get_obj( Build::KEY_OPT_CPP ) if !saved_opt
if cpp_opt != saved_opt # options differ
log.info "%s out-of-date since pre-processor options differ" % @path
@ood = true
return false
end
# compiler path and CPP options are the same and object file exists
# compare time stamp of object with time stamp of all dependencies
#
h = [] # list of dependency header files
t.deps.each{ |dname|
if !File.exist? dname
log.info "%s out-of-date since dependency file %s is absent" % [@path, dname]
@ood = true
return false
end
# dependency file exists, so get timestamp
d_time = File::stat( dname ).mtime
raise "Unable to get time stamp of #{dname}" if ! d_time
h << dname
next if @mtime > d_time # dependency file is older
# dependency file is newer
log.info "%s out-of-date since dependency file %s is newer" % [@path, dname]
h = nil
@ood = true
return false
}
# this target and all header file dependencies are current, so it does not need to
# be remade; set @no_hdr_deps if there are no header file dependencies or set @hdrs
# to the list of those dependencies
#
@ood = false
if h.empty?
@no_hdr_deps = true
else
@hdrs = h
end
return true
end # set_deps_from_db
# Executed by a separate thread for each object so must be thread-safe (multiple
# threads will, of course, never execute this method on the _same_ object!). Only
# invoked on C and CXX objects (later on AS objects if necessary)
#
def discover_deps # discover dependencies
log = Build.logger
# don't need compiler options, only need pre-processor options since we are
# generating dependencies and not compiling
#
opt = get_cpp_options false
compiler = get_compiler
# if persistence database is unavailable, or if dependencies there are not usable
# regenerate them
#
if !set_deps_from_db( compiler, opt )
# sanity check the source file extension
src_path = @deps[ 0 ].path
src_ext = File.extname src_path # actual source extension
s_ext = get_src_ext # expected source extension
raise "Expected extension %s, got %s" % [s_ext, src_ext] if s_ext !~ src_ext
# run pre-processor to generate file of dependency data; that file has same path
# as object file but with .d extension
# sub! returns nil if no substitution occurred
#
extname = File.extname @path
dpath = @path.dup.sub!( /#{extname}\Z/o, '.d' )
raise "Extension substitution of #{extname} failed on #{@path}" if !dpath
# the -M yields all header files (use -MM to exclude system headers).
# NOTE: We want all headers since we really should recompile if some system header
# changed due to a package upgrade.
#
cmd = [get_compiler, "#{src_path} -M -MF #{dpath}", opt.to_s].join ' '
Util.run_cmd cmd, Build.logger
files = parse_deps dpath # list of file names (not objects)
if files.nil? || files.empty?
log.warn "No header deps for #{@path}"
@no_hdr_deps = true # indication that header deps have been computed
else
@hdrs = files
end
end # db check
cnt = defined?( @hdrs ) ? @hdrs.size : 0
@build.stats.update_deps cnt # update dependency histogram
@build.lock.synchronize { # awaken main thread if all tasks are done
@build.deps_done_cnt += 1
if @build.deps_enq_cnt == @build.deps_done_cnt
@build.done.signal
log.info "All dependency tasks complete; signalled main thread"
end
}
end # discover_deps
# Convert list of header file names into list of objects and add to @deps. Invoked by
# main thread after all worker threads have finished and dependent header file names
# have been discovered and stored in @hdrs
#
# Not thread safe since it accesses and modifies global target list
#
def hdrs_to_deps
return if !defined?( @hdrs ) || @hdrs.empty? # nothing to do
# @hdrs guaranteed to be non-nil and non-empty here
@hdrs.each { |f|
target = @build.find_target f
if target # target object already exists
raise "Multiple targets for #{f}" if target.size > 1
@deps << target.first
next
end
# get here for system headers (which will not be automatically discovered when
# we scan the project source directory)
#
#log.debug "Adding header file target: %s" % f
src = SourceFileTarget.new f, @build # create object
@build.add_targets [src]
@deps << src
} # @hdrs iterator
# no longer need @hdrs
@hdrs = nil
end # hdrs_to_deps
# expected prefixes for all header files
R_HDR_PREFIX = %r{^/(?:usr|opt|home)}o
# OSX gets many headers from strange places:
# /System/Library/Frameworks/...
# /Applications/Xcode.app/Contents/...
#
R_HDR_PREFIX_OSX = %r{^/(?:Developer|System|Users|Applications)}o
def parse_deps dpath # parse dependency file and return array of objects or nil
log = Build.logger
result = []
# We might see output like this:
# Stuff.o: \
# /home/user/src/stuff/c++/Stuff.C \
# /home/user/src/stuff/c++/Stuff.h \
# ...
#
# or like this:
# baz.o: /home/user/c/baz.c /usr/include/stdio.h /usr/include/features.h \
# /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \
# /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
# /usr/include/gnu/stubs-64.h \
# /usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/include/stddef.h \
# /usr/include/bits/types.h /usr/include/bits/typesizes.h \
# /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
# /usr/lib/x86_64-linux-gnu/gcc/x86_64-linux-gnu/4.5.2/include/stdarg.h \
# /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h slow.h
#
state = :first
s_ext = get_src_ext # expected extension on source file
IO.foreach( dpath ) { |line|
line.strip!
fields = line.split
if :first == state
raise "No dependency info: line = %s, dpath = %s" % [line, dpath] if
fields.size < 2
obj = fields.shift # first field should be object file with ':' suffix
raise "Expected #{obj} to end with ':'" if !obj.end_with? ':'
src = fields.shift # second field could be '\' or src file
if '\\' == src
state = :second # we expect to find source file on next line
next
end
raise "Expected %s to end with %s (dpath = %s)" % [src, s_ext, dpath] if
s_ext !~ File.extname( src )
state = :rest
elsif :second == state
src = fields.shift # first field should be src file
raise "Expected %s to end with %s (dpath = %s)" % [src, s_ext, dpath] if
s_ext !~ File.extname( src )
state = :rest
end
fields.pop if '\\' == fields[ -1 ] # ignore continuation
next if fields.empty? # nothing else on this line
# have header files; here we have (:rest == state)
fields.each_with_index { |f, i|
raise "Unexpected backslash in #{line}, i = #{i}" if '\\' == f
# we have a real header file name
# since we exclusively use absolute paths with -I options, all dependencies
# should be absolute paths; this is important since multiple directories
# can have the same file name such as util.h or common.h or conf.h
#
raise "Dependency path #{f} not absolute" if !f.start_with? '/'
# All header file paths must start with one of a handful of prefixes; warn if
# they do not.
#
log.warn "Unusual header file location: #{f}" if
!(f =~ R_HDR_PREFIX || (Build.system.darwin? && f =~ R_HDR_PREFIX_OSX))
result << f
} # fields
} # IO.foreach
if false && log.debug? # dump all headers found (lots of output)
if result.empty?
# should never happen since we've eliminated this case above
log.debug "No headers for #{@path}"
else
log.debug "Found #{result.size} headers for #{@path}:"
result.each_with_index{ |p, i| log.debug "%6i: %s" % [i, p.path]}
end
end
return result.empty? ? nil : result
end # parse_deps
end # ObjFileTargetCLike
class ObjFileTargetC < ObjFileTargetCLike # object file from C source
def initialize a_src, a_path, a_build
super
end # initialize
# get options of appropriate type for this target (local set if it exists, a copy of
# global set otherwise)
#
def get_options copy = true
super :cc, copy
end # get_options
def get_cmd # return command to rebuild this object
cpp_opt, cc_opt = get_cpp_options( false ), get_options( false ) # no need to copy
return [@build.cc, "-o %s -c %s" % [@path, @deps[ 0 ].path],
cpp_opt.to_s, cc_opt.to_s].join ' '
end # get_cmd
def get_src_ext # return expected extension of source file
R_C_EXT
end # get_src_ext
def get_compiler # return compiler to compile source file
@build.cc
end # get_compiler
def update_stats time # user time, system time
@build.stats.update :@num_cc_objs, time
Util.print_status @build.thr_pool.tasks.size # update status message on screen
end # update_stats
end # ObjFileTargetC
# may need to refactor later with a common base class for C and C++
class ObjFileTargetCXX < ObjFileTargetCLike # object file from C++ source
def initialize a_src, a_path, a_build, src_ext = R_CXX_EXT
super
end # initialize
# get options of appropriate type for this target (local set if it exists, a copy of
# global set otherwise)
#
def get_options copy = true
super :cxx, copy
end # get_options
def get_cmd # return command to rebuild this object
cpp_opt, cxx_opt = get_cpp_options( false ), get_options( false ) # no need to copy
return [@build.cxx, "-o %s -c %s" % [@path, @deps[ 0 ].path],
cpp_opt.to_s, cxx_opt.to_s].join ' '
end # get_cmd
def get_src_ext # return expected extension of source file
R_CXX_EXT
end # get_src_ext
def get_compiler # return compiler to compile source file
@build.cxx
end # get_compiler
def update_stats time # user time, system time
@build.stats.update :@num_cxx_objs, time
Util.print_status @build.thr_pool.tasks.size # update status message on screen
end # update_stats
end # ObjFileTargetCXX
# object file obtained by linking
class Linkable < CompileOrLinkTarget # common base class for libs and executables
# link_type
# NOTE: Fully static linking is no longer possible on many modern systems, see:
# http://stackoverflow.com/questions/3430400/linux-static-linking-is-dead
#
# :static - for a library, this means we build an archive of object files using 'ar';
# for an executable, this means that for any local libraries (i.e. those
# that are part of this project) we use the static versions. System
# libraries are specified as usual with "-l" flags and the linker may use
# dynamic versions for those.
# :dynamic - fully dynamically linked library (libfoo.{so,dylib}) or executable
# :hybrid - (not currently supported)
# library or executable where some components are statically linked and
# some dynamic (possible to do this with linker options like this:
# -Wl,-Bstatic -la -lb -Wl,-Bdynamic -lx -ly
# Here lib{a,b} will be statically linked, lib{x,y} dynamically linked.
# (see http://linux.die.net/man/1/ld for a detailed ld man page)
#
# libs -- list of library dependencies
# linker -- :ld_cc or :ld_cxx
# opt_type -- type of options relevant to this target; one of:
# :ld_cc_lib, :ld_cc_exec, :ld_cxx_lib, :ld_cxx_exec
#
# needs -- transient variable: set of targets that need to be built before this one
# can be built
# lock -- mutex to synchronize access to @needs since multiple threads may remove
# dependencies from this set as they are made
#
attr :link_type, :libs, :linker, :opt_type
attr_accessor :needs, :lock
EXT2TYPE = { # map file extension to build type
'.dbg_d' => :dbg, '.opt_d' => :opt, '.rel_d' => :rel, # executables
'.dbg_s' => :dbg, '.opt_s' => :opt, '.rel_s' => :rel,
'.a_dbg' => :dbg, '.a_opt' => :opt, '.a_rel' => :rel,
'.so_dbg' => :dbg, '.so_opt' => :opt, '.so_rel' => :rel, # libs
'.dylib_dbg' => :dbg, '.dylib_opt' => :opt, '.dylib_rel' => :rel } # OSX libs
# args is a hash with these keys:
#
# path -- path to library or executable (required)
# build -- current build (required)
# deps -- object file dependencies (required)
# libs -- library dependencies (optional)
# linker -- :ld_cc (C link) or :ld_cxx (C++ link) (required)
# link_type -- :static or :dynamic; default is to use value in build (optional)
#
def initialize args
super args[ :path ], args[ :build ], args[ :deps ]
ldeps = args[ :libs ]
@libs = ldeps if ldeps
@linker = args[ :linker ]
raise "Missing linker argument" if !@linker
raise "Bad linker type: #{@linker}" if ![:ld_cc, :ld_cxx].include? @linker
# executable or library file may not yet exist, so cannot stat it
ltype = args[ :link_type ]
if ltype
raise "Bad link_type: #{ltype}" if ![:static, :dynamic].include?( ltype )
@link_type = ltype
else
@link_type = build.link_type
end
# check that build type of file matches global build type
@build_type = EXT2TYPE[ File.extname @path ]
raise "Build type mismatch for #{@path}: #{@build_type} != #{build.build_type}" if
@build_type != build.build_type
# debug
#log = Build.logger
#log.debug "dependencies for #{@path} = "
# @deps.each { |d| log.debug d.class.name + ": #{d.path.size}" }
# check that the extension on dependencies matches type
t = ".o_#{@build_type}" # expected extension of dependency
i = @deps.index{ |d| t != File.extname( d.path ) }
raise "Unexpected extension (expected #{t}) in: #{@deps[ i ].path}" if i
# check extension of all library dependencies
if defined? @libs
e = "_%s" % @build_type
@libs.each { |lib|
ext = File.extname lib.path
raise "Unexpected extension (expected suffix #{e}) in #{lib.path}" if
!ext.end_with? e
}
end
end # initialize
# return true iff this target is out-of-date and needs to be made; since this is a
# linker object, its dependencies could be object files or other libraries. Here we
# set self.need to the set of out-of-date dependencies and add self to d.needed_by
# to each of those dependencies d
#
def out_of_date?
return @ood if defined? @ood
log = Build.logger
if false # debug
log.debug "Checking dependencies (%d libraries, %d files) of %s:" %
[(defined?( @libs ) ? @libs.size : 0), @deps.size, @path]
@libs.each_with_index{ |d, i|
log.debug "%6d: %s" % [i, d.path]
} if defined?( @libs )
@deps.each_with_index{ |d, i|
log.debug "%6d: %s" % [i, d.path]
}
end
# check if we have out-of-date dependencies
all_deps = @deps
all_deps += @libs if defined? @libs
need_to_make_deps = all_deps.select{ |d| d.out_of_date? }
if !File.exist?( @path )
log.info "%s out-of-date since file is absent" % @path
@ood = true
# cannot return yet since we need to process dependencies
else
@mtime = File::stat( @path ).mtime if !defined?( @mtime )
end
if need_to_make_deps.empty?
log.info "All dependencies of %s are current" % @path
# no need to set @needs; determine if this target needs regeneration
if !defined? @ood # file exists; compare mtimes
o_deps = all_deps.select{ |d| @mtime < d.mtime }
if o_deps.empty? # this target is current and does not need to be remade
@ood = false
msg = ("%s is current and will not be remade since file and all " +
"dependencies exist, and file is newer than all deps.") % @path
log.info msg
else # at least 1 dependency is newer
@ood = true
if log.debug? # print list of out-of-date dependencies
log.debug "%s out-of-date since %d dependencies are newer" %
[@path, o_deps.size]
o_deps.map( &:path ).sort.each_with_index{ |s, i|
log.debug " %d: %s" % [i, s]
}
end # log.level check
end # o_deps.empty? check
end # defined? @ood check
else # have at least 1 dependency that is out-of-date; define @needs
@ood = true
@needs = Set[ *need_to_make_deps ]
@lock = Mutex.new
# no need to compare mtime to mtime of all dependencies since we already know
# that this target is out-of-date and will be remade
if log.debug? # print list of out-of-date dependencies
log.debug "%s has %d out-of-date dependencies:" % [@path, @needs.size]
@needs.map( &:path ).sort.each_with_index{ |s, i|
log.debug " %d: %s" % [i, s]
}
end
@needs.each { |d| # for each dependency d, add self to d.needed_by
if !d.needed_by
d.needed_by = Set[ self ]
else
raise "Top-level target %s is a dependency for %s" % [d.path, @path] if
:top == d.needed_by
d.needed_by << self
end
}
end
return @ood
end # out_of_date?
# get options of appropriate type for this target (local set if it exists, a copy of
# global set otherwise)
#
def get_options copy = true
super @opt_type, copy
end # get_options
# Recursive utility routine to add library file paths to list. Used when constructing
# the link command: for example, if the executable is built from object files a.o and
# b.o and libraries libA and libB where the latter depends on libC, we want this
# function to return the list of paths to:
# a.o, b.o, libA.a, libB.a, libC.a {static linking}
# a.o, b.o, libA.so, libB.so, libC.so {dynamic linking}
#