rscons 1.9.3 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,7 +2,6 @@ require "digest/md5"
2
2
  require "fileutils"
3
3
  require "json"
4
4
  require "set"
5
- require "singleton"
6
5
  require "rscons/version"
7
6
 
8
7
  module Rscons
@@ -51,7 +50,6 @@ module Rscons
51
50
  # },
52
51
  # }
53
52
  class Cache
54
- include Singleton
55
53
 
56
54
  # Name of the file to store cache information in
57
55
  CACHE_FILE = ".rsconscache"
@@ -59,6 +57,13 @@ module Rscons
59
57
  # Prefix for phony cache entries.
60
58
  PHONY_PREFIX = ":PHONY:"
61
59
 
60
+ class << self
61
+ # Access the singleton instance.
62
+ def instance
63
+ @instance ||= Cache.new
64
+ end
65
+ end
66
+
62
67
  # Create a Cache object and load in the previous contents from the cache
63
68
  # file.
64
69
  def initialize
@@ -35,6 +35,10 @@ module Rscons
35
35
  rsconsfile = f
36
36
  end
37
37
 
38
+ opts.on("-j NTHREADS", "Use NTHREADS parallel jobs (local default #{Rscons.n_threads})") do |n_threads|
39
+ Rscons.n_threads = n_threads.to_i
40
+ end
41
+
38
42
  opts.on_tail("--version", "Show version") do
39
43
  puts "Rscons version #{Rscons::VERSION}"
40
44
  exit 0
@@ -1,6 +1,7 @@
1
1
  require "fileutils"
2
2
  require "set"
3
3
  require "shellwords"
4
+ require "thwait"
4
5
 
5
6
  module Rscons
6
7
  # The Environment class is the main programmatic interface to Rscons. It
@@ -13,15 +14,15 @@ module Rscons
13
14
  # @return [Symbol] :command, :short, or :off
14
15
  attr_accessor :echo
15
16
 
16
- # @return [String, nil] The build root.
17
+ # @return [String] The build root.
17
18
  attr_reader :build_root
18
19
 
19
20
  # Set the build root.
20
21
  #
21
22
  # @param build_root [String] The build root.
22
23
  def build_root=(build_root)
23
- @build_root = build_root
24
- @build_root.gsub!('\\', '/') if @build_root
24
+ raise "build_root must be non-nil" unless build_root
25
+ @build_root = build_root.gsub("\\", "/")
25
26
  end
26
27
 
27
28
  # Create an Environment object.
@@ -30,15 +31,17 @@ module Rscons
30
31
  # @option options [Symbol] :echo
31
32
  # :command, :short, or :off (default :short)
32
33
  # @option options [String] :build_root
33
- # Build root directory (default nil)
34
+ # Build root directory (default "build")
34
35
  # @option options [Boolean] :exclude_builders
35
36
  # Whether to omit adding default builders (default false)
36
37
  #
37
38
  # If a block is given, the Environment object is yielded to the block and
38
39
  # when the block returns, the {#process} method is automatically called.
39
40
  def initialize(options = {})
41
+ @threaded_commands = Set.new
42
+ @registered_build_dependencies = {}
40
43
  @varset = VarSet.new
41
- @targets = {}
44
+ @job_set = JobSet.new(@registered_build_dependencies)
42
45
  @user_deps = {}
43
46
  @builders = {}
44
47
  @build_dirs = []
@@ -51,7 +54,7 @@ module Rscons
51
54
  end
52
55
  end
53
56
  @echo = options[:echo] || :short
54
- @build_root = options[:build_root]
57
+ @build_root = options[:build_root] || "build"
55
58
 
56
59
  if block_given?
57
60
  yield self
@@ -227,27 +230,40 @@ module Rscons
227
230
  #
228
231
  # This method takes into account the Environment's build directories.
229
232
  #
230
- # @param source_fname [String] Source file name.
231
- # @param suffix [String] Suffix, including "." if desired.
233
+ # @param source_fname [String]
234
+ # Source file name.
235
+ # @param suffix [String]
236
+ # Suffix, including "." if desired.
237
+ # @param options [Hash]
238
+ # Extra options.
239
+ # @option options [Array<String>] :features
240
+ # Builder features to be used for this build. See {#register_builds}.
232
241
  #
233
242
  # @return [String]
234
243
  # The file name to be built from +source_fname+ with suffix +suffix+.
235
- def get_build_fname(source_fname, suffix)
244
+ def get_build_fname(source_fname, suffix, options = {})
236
245
  build_fname = Rscons.set_suffix(source_fname, suffix).gsub('\\', '/')
246
+ options[:features] ||= []
247
+ extra_path = options[:features].include?("shared") ? "/_shared" : ""
237
248
  found_match = @build_dirs.find do |src_dir, obj_dir|
238
249
  if src_dir.is_a?(Regexp)
239
- build_fname.sub!(src_dir, obj_dir)
250
+ build_fname.sub!(src_dir, "#{obj_dir}#{extra_path}")
240
251
  else
241
- build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/")
252
+ build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}#{extra_path}/")
242
253
  end
243
254
  end
244
- if @build_root and not found_match
245
- unless Rscons.absolute_path?(source_fname) or build_fname.start_with?("#{@build_root}/")
246
- build_fname = "#{@build_root}/#{build_fname}"
255
+ unless found_match
256
+ if Rscons.absolute_path?(build_fname)
257
+ if build_fname =~ %r{^(\w):(.*)$}
258
+ build_fname = "#{@build_root}#{extra_path}/_#{$1}#{$2}"
259
+ else
260
+ build_fname = "#{@build_root}#{extra_path}/_#{build_fname}"
261
+ end
262
+ elsif !build_fname.start_with?("#{@build_root}/")
263
+ build_fname = "#{@build_root}#{extra_path}/#{build_fname}"
247
264
  end
248
265
  end
249
- build_fname.gsub!('\\', '/')
250
- build_fname
266
+ build_fname.gsub('\\', '/')
251
267
  end
252
268
 
253
269
  # Get a construction variable's value.
@@ -280,40 +296,62 @@ module Rscons
280
296
  #
281
297
  # @return [void]
282
298
  def process
283
- while @targets.size > 0
284
- expand_paths!
285
- targets = @targets
286
- @targets = {}
287
- cache = Cache.instance
288
- cache.clear_checksum_cache!
289
- targets_processed = Set.new
290
- process_target = proc do |target|
291
- unless targets_processed.include?(target)
292
- targets_processed << target
293
- targets[target].each do |target_params|
294
- target_params[:sources].each do |src|
295
- if targets.include?(src) and not targets_processed.include?(src)
296
- process_target.call(src)
297
- end
299
+ cache = Cache.instance
300
+ begin
301
+ while @job_set.size > 0 or @threaded_commands.size > 0
302
+
303
+ targets_still_building = @threaded_commands.map do |tc|
304
+ tc.build_operation[:target]
305
+ end
306
+ job = @job_set.get_next_job_to_run(targets_still_building)
307
+
308
+ # TODO: have Cache determine when checksums may be invalid based on
309
+ # file size and/or timestamp.
310
+ cache.clear_checksum_cache!
311
+
312
+ if job
313
+ result = run_builder(job[:builder],
314
+ job[:target],
315
+ job[:sources],
316
+ cache,
317
+ job[:vars],
318
+ allow_delayed_execution: true,
319
+ setup_info: job[:setup_info])
320
+ unless result
321
+ raise BuildError.new("Failed to build #{job[:target]}")
322
+ end
323
+ end
324
+
325
+ completed_tcs = Set.new
326
+ # First do a non-blocking wait to pick up any threads that have
327
+ # completed since last time.
328
+ while tc = wait_for_threaded_commands(nonblock: true)
329
+ completed_tcs << tc
330
+ end
331
+
332
+ # If needed, do a blocking wait.
333
+ if (completed_tcs.empty? and job.nil?) or @threaded_commands.size >= Rscons.n_threads
334
+ completed_tcs << wait_for_threaded_commands
335
+ end
336
+
337
+ # Process all completed {ThreadedCommand} objects.
338
+ completed_tcs.each do |tc|
339
+ result = finalize_builder(tc)
340
+ if result
341
+ @build_hooks[:post].each do |build_hook_block|
342
+ build_hook_block.call(tc.build_operation)
298
343
  end
299
- result = run_builder(target_params[:builder],
300
- target,
301
- target_params[:sources],
302
- cache,
303
- target_params[:vars] || {})
304
- unless result
305
- raise BuildError.new("Failed to build #{target}")
344
+ else
345
+ unless @echo == :command
346
+ $stdout.puts "Failed command was: #{command_to_s(tc.command)}"
306
347
  end
348
+ raise BuildError.new("Failed to build #{tc.build_operation[:target]}")
307
349
  end
308
350
  end
351
+
309
352
  end
310
- begin
311
- targets.each_key do |target|
312
- process_target.call(target)
313
- end
314
- ensure
315
- cache.write
316
- end
353
+ ensure
354
+ cache.write
317
355
  end
318
356
  end
319
357
 
@@ -321,7 +359,7 @@ module Rscons
321
359
  #
322
360
  # @return [void]
323
361
  def clear_targets
324
- @targets = {}
362
+ @job_set.clear!
325
363
  end
326
364
 
327
365
  # Expand a construction variable reference.
@@ -354,11 +392,8 @@ module Rscons
354
392
  #
355
393
  # @return [true,false,nil] Return value from Kernel.system().
356
394
  def execute(short_desc, command, options = {})
357
- print_command = proc do
358
- puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
359
- end
360
395
  if @echo == :command
361
- print_command.call
396
+ puts command_to_s(command)
362
397
  elsif @echo == :short
363
398
  puts short_desc
364
399
  end
@@ -366,8 +401,7 @@ module Rscons
366
401
  options_args = options[:options] ? [options[:options]] : []
367
402
  system(*env_args, *Rscons.command_executer, *command, *options_args).tap do |result|
368
403
  unless result or @echo == :command
369
- $stdout.write "Failed command was: "
370
- print_command.call
404
+ $stdout.puts "Failed command was: #{command_to_s(command)}"
371
405
  end
372
406
  end
373
407
  end
@@ -383,12 +417,16 @@ module Rscons
383
417
  def method_missing(method, *args)
384
418
  if @builders.has_key?(method.to_s)
385
419
  target, sources, vars, *rest = args
386
- unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet)
420
+ vars ||= {}
421
+ unless vars.is_a?(Hash) or vars.is_a?(VarSet)
387
422
  raise "Unexpected construction variable set: #{vars.inspect}"
388
423
  end
389
- sources = Array(sources)
390
424
  builder = @builders[method.to_s]
391
- build_target = builder.create_build_target(env: self, target: target, sources: sources)
425
+ target = expand_path(expand_varref(target))
426
+ sources = Array(sources).map do |source|
427
+ expand_path(expand_varref(source))
428
+ end.flatten
429
+ build_target = builder.create_build_target(env: self, target: target, sources: sources, vars: vars)
392
430
  add_target(build_target.to_s, builder, sources, vars, rest)
393
431
  build_target
394
432
  else
@@ -396,25 +434,6 @@ module Rscons
396
434
  end
397
435
  end
398
436
 
399
- # Add a build target.
400
- #
401
- # @param target [String] Build target file name.
402
- # @param builder [Builder] The {Builder} to use to build the target.
403
- # @param sources [Array<String>] Source file name(s).
404
- # @param vars [Hash] Construction variable overrides.
405
- # @param args [Object] Any extra arguments passed to the {Builder}.
406
- #
407
- # @return [void]
408
- def add_target(target, builder, sources, vars, args)
409
- @targets[target] ||= []
410
- @targets[target] << {
411
- builder: builder,
412
- sources: sources,
413
- vars: vars,
414
- args: args,
415
- }
416
- end
417
-
418
437
  # Manually record a given target as depending on the specified files.
419
438
  #
420
439
  # @param target [String,BuildTarget] Target file.
@@ -428,6 +447,42 @@ module Rscons
428
447
  @user_deps[target] = (@user_deps[target] + user_deps).uniq
429
448
  end
430
449
 
450
+ # Manually record the given target(s) as needing to be built after the
451
+ # given prerequisite(s).
452
+ #
453
+ # For example, consider a builder registered to generate gen.c which also
454
+ # generates gen.h as a side-effect. If program.c includes gen.h, then it
455
+ # should not be compiled before gen.h has been generated. When using
456
+ # multiple threads to build, Rscons may attempt to compile program.c before
457
+ # gen.h has been generated because it does not know that gen.h will be
458
+ # generated along with gen.c. One way to prevent that situation would be
459
+ # to first process the Environment with just the code-generation builders
460
+ # in place and then register the compilation builders. Another way is to
461
+ # use this method to record that a certain target should not be built until
462
+ # another has completed. For example, for the situation previously
463
+ # described:
464
+ # env.build_after("program.o", "gen.c")
465
+ #
466
+ # @since 1.10.0
467
+ #
468
+ # @param targets [String, Array<String>]
469
+ # Target files to wait to build until the prerequisites are finished
470
+ # building.
471
+ # @param prerequisites [String, Array<String>]
472
+ # Files that must be built before building the specified targets.
473
+ #
474
+ # @return [void]
475
+ def build_after(targets, prerequisites)
476
+ targets = Array(targets)
477
+ prerequisites = Array(prerequisites)
478
+ targets.each do |target|
479
+ @registered_build_dependencies[target] ||= Set.new
480
+ prerequisites.each do |prerequisite|
481
+ @registered_build_dependencies[target] << prerequisite
482
+ end
483
+ end
484
+ end
485
+
431
486
  # Return the list of user dependencies for a given target.
432
487
  #
433
488
  # @param target [String] Target file name.
@@ -444,6 +499,8 @@ module Rscons
444
499
  #
445
500
  # This method is used internally by Rscons builders.
446
501
  #
502
+ # @deprecated Use {#register_builds} instead.
503
+ #
447
504
  # @param sources [Array<String>] List of source files to build.
448
505
  # @param suffixes [Array<String>]
449
506
  # List of suffixes to try to convert source files into.
@@ -459,8 +516,7 @@ module Rscons
459
516
  converted = nil
460
517
  suffixes.each do |suffix|
461
518
  converted_fname = get_build_fname(source, suffix)
462
- builder = @builders.values.find { |b| b.produces?(converted_fname, source, self) }
463
- if builder
519
+ if builder = find_builder_for(converted_fname, source, [])
464
520
  converted = run_builder(builder, converted_fname, [source], cache, vars)
465
521
  return nil unless converted
466
522
  break
@@ -471,6 +527,54 @@ module Rscons
471
527
  end
472
528
  end
473
529
 
530
+ # Find and register builders to build source files into files containing
531
+ # one of the suffixes given by suffixes.
532
+ #
533
+ # This method is used internally by Rscons builders. It should be called
534
+ # from the builder's #setup method.
535
+ #
536
+ # @since 1.10.0
537
+ #
538
+ # @param target [String]
539
+ # The target that depends on these builds.
540
+ # @param sources [Array<String>]
541
+ # List of source file(s) to build.
542
+ # @param suffixes [Array<String>]
543
+ # List of suffixes to try to convert source files into.
544
+ # @param vars [Hash]
545
+ # Extra variables to pass to the builders.
546
+ # @param options [Hash]
547
+ # Extra options.
548
+ # @option options [Array<String>] :features
549
+ # Set of features the builder must provide. Each feature can be proceeded
550
+ # by a "-" character to indicate that the builder must /not/ provide the
551
+ # given feature.
552
+ # * shared - builder builds a shared object/library
553
+ #
554
+ # @return [Array<String>]
555
+ # List of the output file name(s).
556
+ def register_builds(target, sources, suffixes, vars, options = {})
557
+ options[:features] ||= []
558
+ @registered_build_dependencies[target] ||= Set.new
559
+ sources.map do |source|
560
+ if source.end_with?(*suffixes)
561
+ source
562
+ else
563
+ output_fname = nil
564
+ suffixes.each do |suffix|
565
+ attempt_output_fname = get_build_fname(source, suffix, features: options[:features])
566
+ if builder = find_builder_for(attempt_output_fname, source, options[:features])
567
+ output_fname = attempt_output_fname
568
+ self.__send__(builder.name, output_fname, source, vars)
569
+ @registered_build_dependencies[target] << output_fname
570
+ break
571
+ end
572
+ end
573
+ output_fname or raise "Could not find a builder for #{source.inspect}."
574
+ end
575
+ end
576
+ end
577
+
474
578
  # Invoke a builder to build the given target based on the given sources.
475
579
  #
476
580
  # @param builder [Builder] The Builder to use.
@@ -478,25 +582,66 @@ module Rscons
478
582
  # @param sources [Array<String>] List of source files.
479
583
  # @param cache [Cache] The Cache.
480
584
  # @param vars [Hash] Extra variables to pass to the builder.
585
+ # @param options [Hash]
586
+ # @since 1.10.0
587
+ # Options.
588
+ # @option options [Boolean] :allow_delayed_execution
589
+ # @since 1.10.0
590
+ # Allow a threaded command to be scheduled but not yet completed before
591
+ # this method returns.
592
+ # @option options [Object] :setup_info
593
+ # Arbitrary builder info returned by Builder#setup.
481
594
  #
482
595
  # @return [String,false] Return value from the {Builder}'s +run+ method.
483
- def run_builder(builder, target, sources, cache, vars)
596
+ def run_builder(builder, target, sources, cache, vars, options = {})
484
597
  vars = @varset.merge(vars)
598
+ build_operation = {
599
+ builder: builder,
600
+ target: target,
601
+ sources: sources,
602
+ cache: cache,
603
+ env: self,
604
+ vars: vars,
605
+ setup_info: options[:setup_info]
606
+ }
485
607
  call_build_hooks = lambda do |sec|
486
608
  @build_hooks[sec].each do |build_hook_block|
487
- build_operation = {
488
- builder: builder,
489
- target: target,
490
- sources: sources,
491
- vars: vars,
492
- env: self,
493
- }
494
609
  build_hook_block.call(build_operation)
495
610
  end
496
611
  end
612
+
613
+ # Invoke pre-build hooks.
497
614
  call_build_hooks[:pre]
498
- rv = builder.run(target, sources, cache, self, vars)
499
- call_build_hooks[:post] if rv
615
+
616
+ # Call the builder's #run method.
617
+ if builder.method(:run).arity == 5
618
+ rv = builder.run(*build_operation.values_at(:target, :sources, :cache, :env, :vars))
619
+ else
620
+ rv = builder.run(build_operation)
621
+ end
622
+
623
+ if rv.is_a?(ThreadedCommand)
624
+ # Store the build operation so the post-build hooks can be called
625
+ # with it when the threaded command completes.
626
+ rv.build_operation = build_operation
627
+ start_threaded_command(rv)
628
+ unless options[:allow_delayed_execution]
629
+ # Delayed command execution is not allowed, so we need to execute
630
+ # the command and finalize the builder now.
631
+ tc = wait_for_threaded_commands(which: [rv])
632
+ rv = finalize_builder(tc)
633
+ if rv
634
+ call_build_hooks[:post]
635
+ else
636
+ unless @echo == :command
637
+ $stdout.puts "Failed command was: #{command_to_s(tc.command)}"
638
+ end
639
+ end
640
+ end
641
+ else
642
+ call_build_hooks[:post] if rv
643
+ end
644
+
500
645
  rv
501
646
  end
502
647
 
@@ -505,14 +650,20 @@ module Rscons
505
650
  # Paths beginning with "^/" are expanded by replacing "^" with the
506
651
  # Environment's build root.
507
652
  #
508
- # @param path [String] The path to expand.
653
+ # @param path [String, Array<String>]
654
+ # The path(s) to expand.
509
655
  #
510
- # @return [String] The expanded path.
656
+ # @return [String, Array<String>]
657
+ # The expanded path(s).
511
658
  def expand_path(path)
512
659
  if Rscons.phony_target?(path)
513
660
  path
661
+ elsif path.is_a?(Array)
662
+ path.map do |path|
663
+ expand_path(path)
664
+ end
514
665
  else
515
- path.sub(%r{^\^(?=[\\/])}, @build_root)
666
+ path.sub(%r{^\^(?=[\\/])}, @build_root).gsub("\\", "/")
516
667
  end
517
668
  end
518
669
 
@@ -676,25 +827,164 @@ module Rscons
676
827
 
677
828
  private
678
829
 
679
- # Expand target and source paths before invoking builders.
830
+ # Add a build target.
831
+ #
832
+ # @param target [String] Build target file name.
833
+ # @param builder [Builder] The {Builder} to use to build the target.
834
+ # @param sources [Array<String>] Source file name(s).
835
+ # @param vars [Hash] Construction variable overrides.
836
+ # @param args [Object] Deprecated; unused.
837
+ #
838
+ # @return [void]
839
+ def add_target(target, builder, sources, vars, args)
840
+ setup_info = builder.setup(
841
+ target: target,
842
+ sources: sources,
843
+ env: self,
844
+ vars: vars)
845
+ @job_set.add_job(
846
+ builder: builder,
847
+ target: target,
848
+ sources: sources,
849
+ vars: vars,
850
+ setup_info: setup_info)
851
+ end
852
+
853
+ # Start a threaded command in a new thread.
680
854
  #
681
- # This method expand construction variable references in the target and
682
- # source file names before passing them to the builder. It also expands
683
- # "^/" prefixes to the Environment's build root if a build root is defined.
855
+ # @param tc [ThreadedCommand]
856
+ # The ThreadedCommand to start.
684
857
  #
685
858
  # @return [void]
686
- def expand_paths!
687
- @targets = @targets.reduce({}) do |result, (target, target_params_list)|
688
- target = expand_path(target) if @build_root
689
- target = expand_varref(target)
690
- result[target] = target_params_list.map do |target_params|
691
- sources = target_params[:sources].map do |source|
692
- source = expand_path(source) if @build_root
693
- expand_varref(source)
694
- end.flatten
695
- target_params.merge(sources: sources)
859
+ def start_threaded_command(tc)
860
+ if @echo == :command
861
+ puts command_to_s(tc.command)
862
+ elsif @echo == :short
863
+ if tc.short_description
864
+ puts tc.short_description
865
+ end
866
+ end
867
+
868
+ env_args = tc.system_env ? [tc.system_env] : []
869
+ options_args = tc.system_options ? [tc.system_options] : []
870
+ system_args = [*env_args, *Rscons.command_executer, *tc.command, *options_args]
871
+
872
+ tc.thread = Thread.new do
873
+ system(*system_args)
874
+ end
875
+ @threaded_commands << tc
876
+ end
877
+
878
+ # Wait for threaded commands to complete.
879
+ #
880
+ # @param options [Hash]
881
+ # Options.
882
+ # @option options [Set<ThreadedCommand>, Array<ThreadedCommand>] :which
883
+ # Which {ThreadedCommand} objects to wait for. If not specified, this
884
+ # method will wait for any.
885
+ # @option options [Boolean] :nonblock
886
+ # Set to true to not block.
887
+ #
888
+ # @return [ThreadedCommand, nil]
889
+ # The {ThreadedCommand} object that is finished.
890
+ def wait_for_threaded_commands(options = {})
891
+ options[:which] ||= @threaded_commands
892
+ threads = options[:which].map(&:thread)
893
+ if finished_thread = find_finished_thread(threads, options[:nonblock])
894
+ threaded_command = @threaded_commands.find do |tc|
895
+ tc.thread == finished_thread
896
+ end
897
+ @threaded_commands.delete(threaded_command)
898
+ threaded_command
899
+ end
900
+ end
901
+
902
+ # Check if any of the requested threads are finished.
903
+ #
904
+ # @param threads [Array<Thread>]
905
+ # The threads to check.
906
+ # @param nonblock [Boolean]
907
+ # Whether to be non-blocking. If true, nil will be returned if no thread
908
+ # is finished. If false, the method will wait until one of the threads
909
+ # is finished.
910
+ #
911
+ # @return [Thread, nil]
912
+ # The finished thread, if any.
913
+ def find_finished_thread(threads, nonblock)
914
+ if nonblock
915
+ threads.find do |thread|
916
+ !thread.alive?
917
+ end
918
+ else
919
+ if threads.empty?
920
+ raise "No threads to wait for"
921
+ end
922
+ ThreadsWait.new(*threads).next_wait
923
+ end
924
+ end
925
+
926
+ # Return a string representation of a command.
927
+ #
928
+ # @param command [Array<String>]
929
+ # The command.
930
+ #
931
+ # @return [String]
932
+ # The string representation of the command.
933
+ def command_to_s(command)
934
+ command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
935
+ end
936
+
937
+ # Call a builder's #finalize method after a ThreadedCommand terminates.
938
+ #
939
+ # @param tc [ThreadedCommand]
940
+ # The ThreadedCommand returned from the builder's #run method.
941
+ #
942
+ # @return [String, false]
943
+ # Result of Builder#finalize.
944
+ def finalize_builder(tc)
945
+ tc.build_operation[:builder].finalize(
946
+ tc.build_operation.merge(
947
+ command_status: tc.thread.value,
948
+ tc: tc))
949
+ end
950
+
951
+ # Find a builder that meets the requested features and produces a target
952
+ # of the requested name.
953
+ #
954
+ # @param target [String]
955
+ # Target file name.
956
+ # @param source [String]
957
+ # Source file name.
958
+ # @param features [Array<String>]
959
+ # See {#register_builds}.
960
+ #
961
+ # @return [Builder, nil]
962
+ # The builder found, if any.
963
+ def find_builder_for(target, source, features)
964
+ @builders.values.find do |builder|
965
+ features_met?(builder, features) and builder.produces?(target, source, self)
966
+ end
967
+ end
968
+
969
+ # Determine if a builder meets the requested features.
970
+ #
971
+ # @param builder [Builder]
972
+ # The builder.
973
+ # @param features [Array<String>]
974
+ # See {#register_builds}.
975
+ #
976
+ # @return [Boolean]
977
+ # Whether the builder meets the requested features.
978
+ def features_met?(builder, features)
979
+ builder_features = builder.features
980
+ features.all? do |feature|
981
+ want_feature = true
982
+ if feature =~ /^-(.*)$/
983
+ want_feature = false
984
+ feature = $1
696
985
  end
697
- result
986
+ builder_has_feature = builder_features.include?(feature)
987
+ want_feature ? builder_has_feature : !builder_has_feature
698
988
  end
699
989
  end
700
990