rscons 1.9.3 → 1.10.0

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.
@@ -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