From b882f8de99f2aadee381ed01d2d7ecdc73cbb274 Mon Sep 17 00:00:00 2001 From: Josh Holtrop Date: Sun, 17 Feb 2019 22:08:39 -0500 Subject: [PATCH] Rework builder interface to only use #run method - close #91 The builder's #run method will be called repeatedly until it returns true or false. The Builder#wait_for method can be used to cause a builder to wait for a Thread, Command, or another Builder. --- build_tests/custom_builder/Rsconscript | 2 +- build_tests/custom_builder/cvar_expansion.rb | 2 +- build_tests/custom_builder/cvar_lambda.rb | 2 +- .../custom_builder/error_run_return_value.rb | 8 + build_tests/custom_builder/error_wait_for.rb | 8 + .../custom_builder/multiple_targets.rb | 2 +- build_tests/custom_builder/wait_for_thread.rb | 21 ++ build_tests/json_to_yaml/Rsconscript | 2 +- build_tests/simple/builder_no_sources.rb | 2 +- build_tests/simple/cache_debugging.rb | 44 +-- build_tests/simple/cache_varset.rb | 2 +- build_tests/simple/phony_target.rb | 2 +- build_tests/simple/threading.rb | 15 +- build_tests/simple/user_dep_build_order.rb | 18 +- .../simple/wait_for_builds_on_failure.rb | 15 +- build_tests/two_sources/cache_strict_deps.rb | 16 +- build_tests/typical/echo_command_string.rb | 2 +- lib/rscons.rb | 2 +- lib/rscons/builder.rb | 146 ++++---- lib/rscons/builder_builder.rb | 10 +- lib/rscons/builders/cfile.rb | 41 +-- lib/rscons/builders/command.rb | 41 +-- lib/rscons/builders/directory.rb | 4 +- lib/rscons/builders/disassemble.rb | 25 +- lib/rscons/builders/install.rb | 2 +- lib/rscons/builders/library.rb | 41 +-- lib/rscons/builders/object.rb | 48 +-- lib/rscons/builders/preprocess.rb | 56 ++- lib/rscons/builders/program.rb | 52 +-- lib/rscons/builders/shared_library.rb | 63 +--- lib/rscons/builders/shared_object.rb | 48 +-- lib/rscons/builders/simple_builder.rb | 9 +- lib/rscons/command.rb | 66 ++++ lib/rscons/environment.rb | 330 +++++++++--------- lib/rscons/threaded_command.rb | 61 ---- spec/build_tests_spec.rb | 23 ++ spec/rscons/builders/simple_builder_spec.rb | 12 - spec/rscons/environment_spec.rb | 2 +- 38 files changed, 580 insertions(+), 665 deletions(-) create mode 100644 build_tests/custom_builder/error_run_return_value.rb create mode 100644 build_tests/custom_builder/error_wait_for.rb create mode 100644 build_tests/custom_builder/wait_for_thread.rb create mode 100644 lib/rscons/command.rb delete mode 100644 lib/rscons/threaded_command.rb delete mode 100644 spec/rscons/builders/simple_builder_spec.rb diff --git a/build_tests/custom_builder/Rsconscript b/build_tests/custom_builder/Rsconscript index b1ab7d2..73e9a89 100644 --- a/build_tests/custom_builder/Rsconscript +++ b/build_tests/custom_builder/Rsconscript @@ -5,7 +5,7 @@ class MySource < Rscons::Builder #define THE_VALUE 5678 EOF end - @target + true end end diff --git a/build_tests/custom_builder/cvar_expansion.rb b/build_tests/custom_builder/cvar_expansion.rb index cb13bee..ef8cc48 100644 --- a/build_tests/custom_builder/cvar_expansion.rb +++ b/build_tests/custom_builder/cvar_expansion.rb @@ -5,7 +5,7 @@ class MySource < Rscons::Builder #define THE_VALUE 678 EOF end - @target + true end end diff --git a/build_tests/custom_builder/cvar_lambda.rb b/build_tests/custom_builder/cvar_lambda.rb index 2606c9f..33e341b 100644 --- a/build_tests/custom_builder/cvar_lambda.rb +++ b/build_tests/custom_builder/cvar_lambda.rb @@ -5,7 +5,7 @@ class MySource < Rscons::Builder #define THE_VALUE #{@env.expand_varref("${the_value}")} EOF end - @target + true end end diff --git a/build_tests/custom_builder/error_run_return_value.rb b/build_tests/custom_builder/error_run_return_value.rb new file mode 100644 index 0000000..bae5ef7 --- /dev/null +++ b/build_tests/custom_builder/error_run_return_value.rb @@ -0,0 +1,8 @@ +build do + Environment.new do |env| + env.add_builder(:MyBuilder) do |options| + "hi" + end + env.MyBuilder("foo") + end +end diff --git a/build_tests/custom_builder/error_wait_for.rb b/build_tests/custom_builder/error_wait_for.rb new file mode 100644 index 0000000..da93ec9 --- /dev/null +++ b/build_tests/custom_builder/error_wait_for.rb @@ -0,0 +1,8 @@ +build do + Environment.new do |env| + env.add_builder(:MyBuilder) do |options| + wait_for(1) + end + env.MyBuilder("foo") + end +end diff --git a/build_tests/custom_builder/multiple_targets.rb b/build_tests/custom_builder/multiple_targets.rb index a4001b2..73267dd 100644 --- a/build_tests/custom_builder/multiple_targets.rb +++ b/build_tests/custom_builder/multiple_targets.rb @@ -8,7 +8,7 @@ class CHGen < Rscons::Builder File.open(h_fname, "w") {|fh| fh.puts "extern int THE_VALUE;"} @cache.register_build([c_fname, h_fname], "", @sources, @env) end - @target + true end end diff --git a/build_tests/custom_builder/wait_for_thread.rb b/build_tests/custom_builder/wait_for_thread.rb new file mode 100644 index 0000000..e63fc20 --- /dev/null +++ b/build_tests/custom_builder/wait_for_thread.rb @@ -0,0 +1,21 @@ +class MyBuilder < Rscons::Builder + def run(options) + if @thread + true + else + @env.print_builder_run_message("#{name} #{target}", nil) + @thread = Thread.new do + sleep 2 + FileUtils.touch(@target) + end + wait_for(@thread) + end + end +end + +build do + Environment.new do |env| + env.add_builder(MyBuilder) + env.MyBuilder("foo") + end +end diff --git a/build_tests/json_to_yaml/Rsconscript b/build_tests/json_to_yaml/Rsconscript index b7773bd..0c3d2fa 100644 --- a/build_tests/json_to_yaml/Rsconscript +++ b/build_tests/json_to_yaml/Rsconscript @@ -10,7 +10,7 @@ build do end @cache.register_build(@target, :JsonToYaml, @sources, @env) end - @target + true end env.JsonToYaml('foo.yml', 'foo.json') end diff --git a/build_tests/simple/builder_no_sources.rb b/build_tests/simple/builder_no_sources.rb index 81ceb4c..4c3b7c1 100644 --- a/build_tests/simple/builder_no_sources.rb +++ b/build_tests/simple/builder_no_sources.rb @@ -1,6 +1,6 @@ class TestBuilder < Rscons::Builder def run(options) - target + true end end build do diff --git a/build_tests/simple/cache_debugging.rb b/build_tests/simple/cache_debugging.rb index 9b25c6a..df58ff3 100644 --- a/build_tests/simple/cache_debugging.rb +++ b/build_tests/simple/cache_debugging.rb @@ -1,30 +1,30 @@ class DebugBuilder < Rscons::Builder def run(options) - command = %W[gcc -c -o #{@target} #{@sources.first}] - if Rscons.vars["command_change"] - command += %w[-Wall] - end - if Rscons.vars["new_dep"] - @sources += ["extra"] - end - if Rscons.vars["strict_deps1"] - @sources += ["extra"] - strict_deps = true - end - if Rscons.vars["strict_deps2"] - @sources = ["extra"] + @sources - strict_deps = true - end - if @cache.up_to_date?(@target, command, @sources, @env, debug: true, strict_deps: strict_deps) - @target + if @command + finalize_command else - ThreadedCommand.new(command, short_description: "#{name} #{@target}") + @command = %W[gcc -c -o #{@target} #{@sources.first}] + if Rscons.vars["command_change"] + @command += %w[-Wall] + end + if Rscons.vars["new_dep"] + @sources += ["extra"] + end + if Rscons.vars["strict_deps1"] + @sources += ["extra"] + strict_deps = true + end + if Rscons.vars["strict_deps2"] + @sources = ["extra"] + @sources + strict_deps = true + end + if @cache.up_to_date?(@target, @command, @sources, @env, debug: true, strict_deps: strict_deps) + true + else + register_command("#{name} #{target}", @command) + end end end - - def finalize(options) - standard_finalize(options) - end end build do diff --git a/build_tests/simple/cache_varset.rb b/build_tests/simple/cache_varset.rb index 6895d89..29fb33a 100644 --- a/build_tests/simple/cache_varset.rb +++ b/build_tests/simple/cache_varset.rb @@ -9,7 +9,7 @@ class TestBuilder < Rscons::Builder @env.print_builder_run_message(msg, msg) @cache.register_build(@target, command, @sources, @env) end - @target + true end end diff --git a/build_tests/simple/phony_target.rb b/build_tests/simple/phony_target.rb index 84fe0fd..4263a7c 100644 --- a/build_tests/simple/phony_target.rb +++ b/build_tests/simple/phony_target.rb @@ -5,7 +5,7 @@ build do puts "Checker #{@sources.first}" if @env.echo != :off @cache.register_build(@target, :Checker, @sources, @env) end - @target + true end env.Program("simple.exe", "simple.c") env.Checker(:checker, "simple.exe") diff --git a/build_tests/simple/threading.rb b/build_tests/simple/threading.rb index 3920908..65f88a4 100644 --- a/build_tests/simple/threading.rb +++ b/build_tests/simple/threading.rb @@ -1,12 +1,11 @@ class ThreadedTestBuilder < Rscons::Builder def run(options) - command = ["ruby", "-e", %[sleep 1]] - Rscons::ThreadedCommand.new( - command, - short_description: "ThreadedTestBuilder #{@target}") - end - def finalize(options) - true + if @command + true + else + @command = ["ruby", "-e", %[sleep 1]] + register_command("ThreadedTestBuilder #{@target}", @command) + end end end @@ -14,7 +13,7 @@ class NonThreadedTestBuilder < Rscons::Builder def run(options) puts "NonThreadedTestBuilder #{@target}" sleep 1 - @target + true end end diff --git a/build_tests/simple/user_dep_build_order.rb b/build_tests/simple/user_dep_build_order.rb index ad4eb85..1fe62ac 100644 --- a/build_tests/simple/user_dep_build_order.rb +++ b/build_tests/simple/user_dep_build_order.rb @@ -1,15 +1,15 @@ class TestBuilder < Rscons::Builder def run(options) - if @target == "two" - return false unless File.exists?("one") + if @command + true + else + if @target == "two" + return false unless File.exists?("one") + end + wait_time = @env.expand_varref("${wait_time}", @vars) + @command = ["ruby", "-e", "require 'fileutils'; sleep #{wait_time}; FileUtils.touch('#{@target}');"] + register_command("TestBuilder", @command) end - wait_time = @env.expand_varref("${wait_time}", @vars) - command = ["ruby", "-e", "require 'fileutils'; sleep #{wait_time}; FileUtils.touch('#{@target}');"] - standard_threaded_build("TestBuilder", @target, command, [], @env, @cache) - end - - def finalize(options) - standard_finalize(options) end end diff --git a/build_tests/simple/wait_for_builds_on_failure.rb b/build_tests/simple/wait_for_builds_on_failure.rb index 065d99a..7e8025b 100644 --- a/build_tests/simple/wait_for_builds_on_failure.rb +++ b/build_tests/simple/wait_for_builds_on_failure.rb @@ -1,12 +1,13 @@ class Fail < Rscons::Builder def run(options) - wait_time = @env.expand_varref("${wait_time}", @vars) - ruby_command = %[sleep #{wait_time}; exit 2] - command = %W[ruby -e #{ruby_command}] - standard_threaded_build("Fail #{@target}", @target, command, [], @env, @cache) - end - def finalize(options) - standard_finalize(options) + if @command + finalize_command + else + wait_time = @env.expand_varref("${wait_time}", @vars) + ruby_command = %[sleep #{wait_time}; exit 2] + @command = %W[ruby -e #{ruby_command}] + register_command("Fail #{@target}", @command) + end end end diff --git a/build_tests/two_sources/cache_strict_deps.rb b/build_tests/two_sources/cache_strict_deps.rb index 2c4a6b1..045d45e 100644 --- a/build_tests/two_sources/cache_strict_deps.rb +++ b/build_tests/two_sources/cache_strict_deps.rb @@ -1,16 +1,16 @@ class StrictBuilder < Rscons::Builder def run(options) - command = %W[gcc -o #{@target}] + @sources.sort - if @cache.up_to_date?(@target, command, @sources, @env, strict_deps: true) - @target + if @command + finalize_command else - ThreadedCommand.new(command, short_description: "#{name} #{@target}") + @command = %W[gcc -o #{@target}] + @sources.sort + if @cache.up_to_date?(@target, @command, @sources, @env, strict_deps: true) + true + else + register_command("#{name} #{@target}", @command) + end end end - - def finalize(options) - standard_finalize(options) - end end build do diff --git a/build_tests/typical/echo_command_string.rb b/build_tests/typical/echo_command_string.rb index 6ba6c38..a5ed4ca 100644 --- a/build_tests/typical/echo_command_string.rb +++ b/build_tests/typical/echo_command_string.rb @@ -1,7 +1,7 @@ class MyBuilder < Rscons::Builder def run(options) @env.print_builder_run_message("MyBuilder #{@target}", "MyBuilder #{@target} command") - @target + true end end diff --git a/lib/rscons.rb b/lib/rscons.rb index 6027d48..5f6cf82 100644 --- a/lib/rscons.rb +++ b/lib/rscons.rb @@ -6,9 +6,9 @@ require_relative "rscons/builder_builder" require_relative "rscons/builder_set" require_relative "rscons/cache" require_relative "rscons/configure_op" +require_relative "rscons/command" require_relative "rscons/environment" require_relative "rscons/script" -require_relative "rscons/threaded_command" require_relative "rscons/util" require_relative "rscons/varset" require_relative "rscons/version" diff --git a/lib/rscons/builder.rb b/lib/rscons/builder.rb index d87f3c4..aa96ce6 100644 --- a/lib/rscons/builder.rb +++ b/lib/rscons/builder.rb @@ -6,6 +6,7 @@ module Rscons # Class to hold an object that knows how to build a certain type of file. class Builder + class << self # Return the name of the builder. # @@ -108,104 +109,97 @@ module Rscons # @param options [Hash] # Run options. # - # @return [ThreadedCommand,String,false] - # Name of the target file on success or false on failure. - # Since 1.10.0, this method may return an instance of {ThreadedCommand}. - # In that case, the build operation has not actually been completed yet - # but the command to do so will be executed by Rscons in a separate - # thread. This allows for build parallelization. If a {ThreadedCommand} - # object is returned, the {#finalize} method will be called after the - # command has completed. The {#finalize} method should then be used to - # record cache info, if needed, and to return the true result of the - # build operation. The builder can store information to be passed in to - # the {#finalize} method by populating the :builder_info field of the - # {ThreadedCommand} object returned here. + # @return [Object] + # If the build operation fails, this method should return +false+. + # If the build operation succeeds, this method should return +true+. + # If the build operation is not yet complete and is waiting on other + # operations, this method should return the return value from the + # {#wait_for} method. def run(options) raise "This method must be overridden in a subclass" end - # Finalize a build operation. + # Create a {Command} object to execute the build command in a thread. # - # This method is called after the {#run} method if the {#run} method - # returns a {ThreadedCommand} object. - # - # @since 1.10.0 - # - # @param options [Hash] - # Options. - # @option options [String] :target - # Target file name. - # @option options [Array] :sources - # Source file name(s). - # @option options [Cache] :cache - # The Cache object. - # @option options [Environment] :env - # The Environment executing the builder. - # @option options [Hash,VarSet] :vars - # Extra construction variables. - # @option options [true,false,nil] :command_status - # If the {#run} method returns a {ThreadedCommand}, this field will - # contain the return value from executing the command with - # Kernel.system(). - # @option options [ThreadedCommand] :tc - # The {ThreadedCommand} object that was returned by the #run method. - # - # @return [String,false] - # Name of the target file on success or false on failure. - def finalize(options) - end - - # Check if the cache is up to date for the target and if not create a - # {ThreadedCommand} object to execute the build command in a thread. - # - # @since 1.10.0 - # - # @param short_cmd_string [String] + # @param short_description [String] # Short description of build action to be printed when env.echo == # :short. - # @param target [String] Name of the target file. # @param command [Array] - # The command to execute to build the target. - # @param sources [Array] Source file name(s). - # @param env [Environment] The Environment executing the builder. - # @param cache [Cache] The Cache object. - # @param options [Hash] Options. + # The command to execute. + # @param options [Hash] + # Options. # @option options [String] :stdout # File name to redirect standard output to. # - # @return [String,ThreadedCommand] - # The name of the target if it is already up to date or the - # {ThreadedCommand} object created to update it. - def standard_threaded_build(short_cmd_string, target, command, sources, env, cache, options = {}) - if cache.up_to_date?(target, command, sources, env) - target + # @return [Object] + # Return value for {#run} method. + def register_command(short_description, command, options = {}) + command_options = {} + if options[:stdout] + command_options[:system_options] = {out: options[:stdout]} + end + @env.print_builder_run_message(short_description, @command) + wait_for(Command.new(command, self, command_options)) + end + + # Check if the cache is up to date for the target and if not create a + # {Command} object to execute the build command in a thread. + # + # @param short_description [String] + # Short description of build action to be printed when env.echo == + # :short. + # @param command [Array] + # The command to execute. + # @param options [Hash] + # Options. + # @option options [Array] :sources + # Sources to override @sources. + # @option options [String] :stdout + # File name to redirect standard output to. + # + # @return [Object] + # Return value for {#run} method. + def standard_command(short_description, command, options = {}) + @command = command + sources = options[:sources] || @sources + if @cache.up_to_date?(@target, @command, sources, @env) + true else - unless Rscons.phony_target?(target) - cache.mkdir_p(File.dirname(target)) - FileUtils.rm_f(target) + unless Rscons.phony_target?(@target) + @cache.mkdir_p(File.dirname(@target)) + FileUtils.rm_f(@target) end - tc_options = {short_description: short_cmd_string} - if options[:stdout] - tc_options[:system_options] = {out: options[:stdout]} - end - ThreadedCommand.new(command, tc_options) + register_command(short_description, @command, options) end end - # Register build results from a {ThreadedCommand} with the cache. + # Register build results from a {Command} with the cache. # # @since 1.10.0 # # @param options [Hash] # Builder finalize options. + # @option options [String] :stdout + # File name to redirect standard output to. # - # @return [String, nil] - # The target name on success or nil on failure. - def standard_finalize(options) - if options[:command_status] - @cache.register_build(@target, options[:tc].command, @sources, @env) - @target - end + # @return [true] + # Return value for {#run} method. + def finalize_command(options = {}) + sources = options[:sources] || @sources + @cache.register_build(@target, @command, sources, @env) + true end + + # A builder can indicate to Rscons that it needs to wait for a separate + # operation to complete by using this method. The return value from this + # method should be returned from the builder's #run method. + # + # @param things [Builder, Command, Thread, Array] + # Builder(s) or Command(s) or Thread(s) that this builder needs to wait + # for. + def wait_for(things) + Array(things) + end + end end diff --git a/lib/rscons/builder_builder.rb b/lib/rscons/builder_builder.rb index 67e2ed8..9a450f6 100644 --- a/lib/rscons/builder_builder.rb +++ b/lib/rscons/builder_builder.rb @@ -3,21 +3,15 @@ module Rscons # it is needed. class BuilderBuilder - # @return [String] Builder name. - attr_reader :name - # Create a BuilderBuilder. # - # @param name [String] - # Builder name. # @param builder_class [Class] # The {Builder} class to be instantiated. # @param builder_args [Array] # Any extra arguments to be passed to the builder class. # @param builder_block [Proc, nil] # Optional block to be passed to the {Builder} class's #new method. - def initialize(name, builder_class, *builder_args, &builder_block) - @name = name + def initialize(builder_class, *builder_args, &builder_block) @builder_class = builder_class @builder_args = builder_args @builder_block = builder_block @@ -26,7 +20,7 @@ module Rscons # Act like a regular {Builder} class object but really instantiate the # requested {Builder} class, potentially with extra arguments and a block. def new(*args) - @builder_class.new(*args, *@builder_args, &@builder_block) + @builder_class.new(*@builder_args, *args, &@builder_block) end end diff --git a/lib/rscons/builders/cfile.rb b/lib/rscons/builders/cfile.rb index 475c4e2..539a9cf 100644 --- a/lib/rscons/builders/cfile.rb +++ b/lib/rscons/builders/cfile.rb @@ -21,30 +21,23 @@ module Rscons # Run the builder to produce a build target. def run(options) - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @sources - cmd = - case - when @sources.first.end_with?(*@env.expand_varref("${LEXSUFFIX}")) - "LEX" - when @sources.first.end_with?(*@env.expand_varref("${YACCSUFFIX}")) - "YACC" - else - raise "Unknown source file #{@sources.first.inspect} for CFile builder" - end - command = @env.build_command("${#{cmd}_CMD}", @vars) - standard_threaded_build("#{cmd} #{@target}", @target, command, @sources, @env, @cache) - end - - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - standard_finalize(options) + if @command + finalize_command + else + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @sources + cmd = + case + when @sources.first.end_with?(*@env.expand_varref("${LEXSUFFIX}")) + "LEX" + when @sources.first.end_with?(*@env.expand_varref("${YACCSUFFIX}")) + "YACC" + else + raise "Unknown source file #{@sources.first.inspect} for CFile builder" + end + command = @env.build_command("${#{cmd}_CMD}", @vars) + standard_command("#{cmd} #{@target}", command) + end end end diff --git a/lib/rscons/builders/command.rb b/lib/rscons/builders/command.rb index 042662f..6bdcc58 100644 --- a/lib/rscons/builders/command.rb +++ b/lib/rscons/builders/command.rb @@ -1,7 +1,7 @@ module Rscons module Builders - # Execute a command that will produce the given target based on the given - # sources. + # A builder to execute an arbitrary command that will produce the given + # target based on the given sources. # # @since 1.8.0 # @@ -11,33 +11,20 @@ module Rscons class Command < Builder # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String, ThreadedCommand] - # Target file name if target is up to date or a {ThreadedCommand} - # to execute to build the target. def run(options) - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @sources - command = @env.build_command("${CMD}", @vars) - cmd_desc = @vars["CMD_DESC"] || "Command" - options = {} - if @vars["CMD_STDOUT"] - options[:stdout] = @env.expand_varref("${CMD_STDOUT}", @vars) + if @command + finalize_command + else + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @sources + command = @env.build_command("${CMD}", @vars) + cmd_desc = @vars["CMD_DESC"] || "Command" + options = {} + if @vars["CMD_STDOUT"] + options[:stdout] = @env.expand_varref("${CMD_STDOUT}", @vars) + end + standard_command("#{cmd_desc} #{@target}", command, options) end - standard_threaded_build("#{cmd_desc} #{@target}", @target, command, @sources, @env, @cache, options) - end - - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - standard_finalize(options) end end diff --git a/lib/rscons/builders/directory.rb b/lib/rscons/builders/directory.rb index fb3e42c..65e4196 100644 --- a/lib/rscons/builders/directory.rb +++ b/lib/rscons/builders/directory.rb @@ -6,14 +6,14 @@ module Rscons # Run the builder to produce a build target. def run(options) if File.directory?(@target) - @target + true elsif File.exists?(@target) Ansi.write($stderr, :red, "Error: `#{@target}' already exists and is not a directory", :reset, "\n") false else @env.print_builder_run_message("Directory #{@target}", nil) @cache.mkdir_p(@target) - @target + true end end diff --git a/lib/rscons/builders/disassemble.rb b/lib/rscons/builders/disassemble.rb index dae87d5..bdfd0f0 100644 --- a/lib/rscons/builders/disassemble.rb +++ b/lib/rscons/builders/disassemble.rb @@ -11,30 +11,15 @@ module Rscons # Run the builder to produce a build target. def run(options) - @vars["_SOURCES"] = sources - command = @env.build_command("${DISASM_CMD}", @vars) - if @cache.up_to_date?(@target, command, @sources, @env) - @target + if @command + finalize_command else - @cache.mkdir_p(File.dirname(@target)) - ThreadedCommand.new( - command, - short_description: "Disassemble #{@target}", - system_options: {out: @target}) + @vars["_SOURCES"] = @sources + command = @env.build_command("${DISASM_CMD}", @vars) + standard_command("Disassemble #{target}", command, stdout: @target) end end - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - standard_finalize(options) - end - end end end diff --git a/lib/rscons/builders/install.rb b/lib/rscons/builders/install.rb index b80b840..1800460 100644 --- a/lib/rscons/builders/install.rb +++ b/lib/rscons/builders/install.rb @@ -42,7 +42,7 @@ module Rscons end @cache.register_build(dest, :Copy, [src], @env) end - @target if (target_is_dir ? Dir.exists?(@target) : File.exists?(@target)) + (target_is_dir ? Dir.exists?(@target) : File.exists?(@target)) ? true : false end end diff --git a/lib/rscons/builders/library.rb b/lib/rscons/builders/library.rb index 88de87f..e36739f 100644 --- a/lib/rscons/builders/library.rb +++ b/lib/rscons/builders/library.rb @@ -11,17 +11,6 @@ module Rscons ) # Create an instance of the Builder to build a target. - # - # @param options [Hash] - # Options. - # @option options [String] :target - # Target file name. - # @option options [Array] :sources - # Source file name(s). - # @option options [Environment] :env - # The Environment executing the builder. - # @option options [Hash,VarSet] :vars - # Extra construction variables. def initialize(options) super(options) suffixes = @env.expand_varref(["${OBJSUFFIX}", "${LIBSUFFIX}"], @vars) @@ -30,29 +19,15 @@ module Rscons end # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String,false] - # Name of the target file on success or false on failure. def run(options) - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @objects - command = @env.build_command("${ARCMD}", @vars) - standard_threaded_build("AR #{@target}", @target, command, @objects, @env, @cache) - end - - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - if options[:command_status] - @cache.register_build(@target, options[:tc].command, @objects, @env) - @target + if @command + finalize_command(sources: @objects) + true + else + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @objects + command = @env.build_command("${ARCMD}", @vars) + standard_command("AR #{@target}", command, sources: @objects) end end diff --git a/lib/rscons/builders/object.rb b/lib/rscons/builders/object.rb index 98e5618..9ebbab2 100644 --- a/lib/rscons/builders/object.rb +++ b/lib/rscons/builders/object.rb @@ -76,40 +76,26 @@ module Rscons end # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String, ThreadedCommand] - # Target file name if target is up to date or a {ThreadedCommand} - # to execute to build the target. def run(options) - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @sources - @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) - com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var| - @sources.first.end_with?(*@env.expand_varref("${#{suffix_var}}", @vars)) - end.tap do |v| - v.nil? and raise "Error: unknown input file type: #{@sources.first.inspect}" - end.first - command = @env.build_command("${#{com_prefix}CMD}", @vars) - @env.produces(@target, @vars["_DEPFILE"]) - standard_threaded_build("#{com_prefix} #{@target}", @target, command, @sources, @env, @cache) - end - - # Finalize the build operation. - # - # @param options [Hash] Builder finalize options. - # - # @return [String, nil] - # Name of the target file on success or nil on failure. - def finalize(options) - if options[:command_status] + if @command deps = @sources - if File.exists?(@vars['_DEPFILE']) - deps += Util.parse_makefile_deps(@vars['_DEPFILE']) + if File.exists?(@vars["_DEPFILE"]) + deps += Util.parse_makefile_deps(@vars["_DEPFILE"]) end - @cache.register_build(@target, options[:tc].command, deps.uniq, @env) - @target + @cache.register_build(@target, @command, deps.uniq, @env) + true + else + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @sources + @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) + com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var| + @sources.first.end_with?(*@env.expand_varref("${#{suffix_var}}", @vars)) + end.tap do |v| + v.nil? and raise "Error: unknown input file type: #{@sources.first.inspect}" + end.first + command = @env.build_command("${#{com_prefix}CMD}", @vars) + @env.produces(@target, @vars["_DEPFILE"]) + standard_command("#{com_prefix} #{@target}", command) end end diff --git a/lib/rscons/builders/preprocess.rb b/lib/rscons/builders/preprocess.rb index befc981..a2db59a 100644 --- a/lib/rscons/builders/preprocess.rb +++ b/lib/rscons/builders/preprocess.rb @@ -12,44 +12,30 @@ module Rscons ) # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String, ThreadedCommand] - # Target file name if target is up to date or a {ThreadedCommand} - # to execute to build the target. def run(options) - if @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} - pp_cc = "${CXX}" - depgen = "${CXXDEPGEN}" - else - pp_cc = "${CC}" - depgen = "${CCDEPGEN}" - end - @vars["_PREPROCESS_CC"] = pp_cc - @vars["_PREPROCESS_DEPGEN"] = depgen - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @sources - @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) - command = @env.build_command("${CPP_CMD}", @vars) - @env.produces(@target, @vars["_DEPFILE"]) - standard_threaded_build("#{name} #{@target}", @target, command, @sources, @env, @cache) - end - - # Finalize the build operation. - # - # @param options [Hash] Builder finalize options. - # - # @return [String, nil] - # Name of the target file on success or nil on failure. - def finalize(options) - if options[:command_status] + if @command deps = @sources - if File.exists?(@vars['_DEPFILE']) - deps += Util.parse_makefile_deps(@vars['_DEPFILE']) + if File.exists?(@vars["_DEPFILE"]) + deps += Util.parse_makefile_deps(@vars["_DEPFILE"]) end - @cache.register_build(@target, options[:tc].command, deps.uniq, @env) - @target + @cache.register_build(@target, @command, deps.uniq, @env) + true + else + if @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} + pp_cc = "${CXX}" + depgen = "${CXXDEPGEN}" + else + pp_cc = "${CC}" + depgen = "${CCDEPGEN}" + end + @vars["_PREPROCESS_CC"] = pp_cc + @vars["_PREPROCESS_DEPGEN"] = depgen + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @sources + @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) + command = @env.build_command("${CPP_CMD}", @vars) + @env.produces(@target, @vars["_DEPFILE"]) + standard_command("#{name} #{@target}", command) end end diff --git a/lib/rscons/builders/program.rb b/lib/rscons/builders/program.rb index ca35d3f..d92b9c1 100644 --- a/lib/rscons/builders/program.rb +++ b/lib/rscons/builders/program.rb @@ -40,40 +40,26 @@ module Rscons end # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String,false] - # Name of the target file on success or false on failure. def run(options) - ld = @env.expand_varref("${LD}", @vars) - ld = if ld != "" - ld - elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${DSUFFIX}", @vars))} - "${DC}" - elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} - "${CXX}" - else - "${CC}" - end - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @objects - @vars["LD"] = ld - command = @env.build_command("${LDCMD}", @vars) - standard_threaded_build("LD #{@target}", @target, command, @objects, @env, @cache) - end - - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - if options[:command_status] - @cache.register_build(@target, options[:tc].command, @objects, @env) - @target + if @command + finalize_command(sources: @objects) + true + else + ld = @env.expand_varref("${LD}", @vars) + ld = if ld != "" + ld + elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${DSUFFIX}", @vars))} + "${DC}" + elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} + "${CXX}" + else + "${CC}" + end + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @objects + @vars["LD"] = ld + command = @env.build_command("${LDCMD}", @vars) + standard_command("LD #{@target}", command, sources: @objects) end end diff --git a/lib/rscons/builders/shared_library.rb b/lib/rscons/builders/shared_library.rb index 11f70de..ca13f82 100644 --- a/lib/rscons/builders/shared_library.rb +++ b/lib/rscons/builders/shared_library.rb @@ -25,17 +25,6 @@ module Rscons end # Create an instance of the Builder to build a target. - # - # @param options [Hash] - # Options. - # @option options [String] :target - # Target file name. - # @option options [Array] :sources - # Source file name(s). - # @option options [Environment] :env - # The Environment executing the builder. - # @option options [Hash,VarSet] :vars - # Extra construction variables. def initialize(options) super(options) libprefix = @env.expand_varref("${SHLIBPREFIX}", @vars) @@ -52,40 +41,26 @@ module Rscons end # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String,false] - # Name of the target file on success or false on failure. def run(options) - ld = @env.expand_varref("${SHLD}", @vars) - ld = if ld != "" - ld - elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${DSUFFIX}", @vars))} - "${SHDC}" - elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} - "${SHCXX}" - else - "${SHCC}" - end - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @objects - @vars["SHLD"] = ld - command = @env.build_command("${SHLDCMD}", @vars) - standard_threaded_build("SHLD #{@target}", @target, command, @objects, @env, @cache) - end - - # Finalize a build. - # - # @param options [Hash] - # Finalize options. - # - # @return [String, nil] - # The target name on success or nil on failure. - def finalize(options) - if options[:command_status] - @cache.register_build(@target, options[:tc].command, @objects, @env) - @target + if @command + finalize_command(sources: @objects) + true + else + ld = @env.expand_varref("${SHLD}", @vars) + ld = if ld != "" + ld + elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${DSUFFIX}", @vars))} + "${SHDC}" + elsif @sources.find {|s| s.end_with?(*@env.expand_varref("${CXXSUFFIX}", @vars))} + "${SHCXX}" + else + "${SHCC}" + end + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @objects + @vars["SHLD"] = ld + command = @env.build_command("${SHLDCMD}", @vars) + standard_command("SHLD #{@target}", command, sources: @objects) end end diff --git a/lib/rscons/builders/shared_object.rb b/lib/rscons/builders/shared_object.rb index 968481a..884f27e 100644 --- a/lib/rscons/builders/shared_object.rb +++ b/lib/rscons/builders/shared_object.rb @@ -61,40 +61,26 @@ module Rscons end # Run the builder to produce a build target. - # - # @param options [Hash] Builder run options. - # - # @return [String, ThreadedCommand] - # Target file name if target is up to date or a {ThreadedCommand} - # to execute to build the target. def run(options) - @vars["_TARGET"] = @target - @vars["_SOURCES"] = @sources - @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) - com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var| - @sources.first.end_with?(*@env.expand_varref("${#{suffix_var}}", @vars)) - end.tap do |v| - v.nil? and raise "Error: unknown input file type: #{@sources.first.inspect}" - end.first - command = @env.build_command("${#{com_prefix}CMD}", @vars) - @env.produces(@target, @vars["_DEPFILE"]) - standard_threaded_build("#{com_prefix} #{@target}", @target, command, @sources, @env, @cache) - end - - # Finalize the build operation. - # - # @param options [Hash] Builder finalize options. - # - # @return [String, nil] - # Name of the target file on success or nil on failure. - def finalize(options) - if options[:command_status] + if @command deps = @sources - if File.exists?(@vars['_DEPFILE']) - deps += Util.parse_makefile_deps(@vars['_DEPFILE']) + if File.exists?(@vars["_DEPFILE"]) + deps += Util.parse_makefile_deps(@vars["_DEPFILE"]) end - @cache.register_build(@target, options[:tc].command, deps.uniq, @env) - @target + @cache.register_build(@target, @command, deps.uniq, @env) + true + else + @vars["_TARGET"] = @target + @vars["_SOURCES"] = @sources + @vars["_DEPFILE"] = Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)) + com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var| + @sources.first.end_with?(*@env.expand_varref("${#{suffix_var}}", @vars)) + end.tap do |v| + v.nil? and raise "Error: unknown input file type: #{@sources.first.inspect}" + end.first + command = @env.build_command("${#{com_prefix}CMD}", @vars) + @env.produces(@target, @vars["_DEPFILE"]) + standard_command("#{com_prefix} #{@target}", command) end end diff --git a/lib/rscons/builders/simple_builder.rb b/lib/rscons/builders/simple_builder.rb index f155ec5..c9800c5 100644 --- a/lib/rscons/builders/simple_builder.rb +++ b/lib/rscons/builders/simple_builder.rb @@ -5,8 +5,14 @@ module Rscons # @since 1.8.0 class SimpleBuilder < Builder + # @return [String] + # User-provided builder name. + attr_reader :name + # Create an instance of the Builder to build a target. # + # @param name [String] + # User-provided builder name. # @param options [Hash] # Options. # @option options [String] :target @@ -20,7 +26,8 @@ module Rscons # @param run_proc [Proc] # A Proc to execute when the builder runs. The provided block must # provide the have the same signature as {Builder#run}. - def initialize(options, &run_proc) + def initialize(name, options, &run_proc) + @name = name super(options) @run_proc = run_proc end diff --git a/lib/rscons/command.rb b/lib/rscons/command.rb new file mode 100644 index 0000000..b23a77b --- /dev/null +++ b/lib/rscons/command.rb @@ -0,0 +1,66 @@ +module Rscons + # Class to keep track of system commands that builders need to execute. + # Rscons will manage scheduling these commands to be run in separate threads. + class Command + + # @return [Builder] + # {Builder} executing this command. + attr_reader :builder + + # @return [Array] + # The command to execute. + attr_reader :command + + # @return [nil, true, false] + # true if the command gives zero exit status. + # false if the command gives non-zero exit status. + # nil if command execution fails. + attr_accessor :status + + # @return [Hash] + # Environment Hash to pass to Kernel#system. + attr_reader :system_env + + # @return [Hash] + # Options Hash to pass to Kernel#system. + attr_reader :system_options + + # @return [Thread] + # The thread waiting on this command to terminate. + attr_reader :thread + + # Create a ThreadedCommand object. + # + # @param command [Array] + # The command to execute. + # @param builder [Builder] + # The {Builder} executing this command. + # @param options [Hash] + # Optional parameters. + # @option options [Hash] :system_env + # Environment Hash to pass to Kernel#system. + # @option options [Hash] :system_options + # Options Hash to pass to Kernel#system. + def initialize(command, builder, options = {}) + @command = command + @builder = builder + @system_env = options[:system_env] + @system_options = options[:system_options] + end + + # Start a thread to run the command. + # + # @return [Thread] + # The Thread created to run the command. + def run + env_args = @system_env ? [@system_env] : [] + options_args = @system_options ? [@system_options] : [] + system_args = [*env_args, *Rscons.command_executer, *@command, *options_args] + + @thread = Thread.new do + system(*system_args) + end + end + + end +end diff --git a/lib/rscons/environment.rb b/lib/rscons/environment.rb index 4209ea9..154e37b 100644 --- a/lib/rscons/environment.rb +++ b/lib/rscons/environment.rb @@ -48,10 +48,9 @@ module Rscons attr_reader :build_root # @return [Integer] - # The number of threads to use for this Environment. If nil (the - # default), the global Rscons.application.n_threads default value will be - # used. - attr_writer :n_threads + # The number of threads to use for this Environment. Defaults to the + # global Rscons.application.n_threads value. + attr_accessor :n_threads # Create an Environment object. # @@ -67,7 +66,8 @@ module Rscons super(options) @id = self.class.get_id self.class.register(self) - @threaded_commands = Set.new + # Hash of Thread object => {Command} or {Builder}. + @threads = {} @registered_build_dependencies = {} @side_effects = {} @builder_set = BuilderSet.new(@registered_build_dependencies, @side_effects) @@ -91,6 +91,7 @@ module Rscons :short end @build_root = "#{Cache.instance.configuration_data["build_dir"]}/e.#{@id}" + @n_threads = Rscons.application.n_threads if block_given? yield self @@ -173,9 +174,12 @@ module Rscons # @return [void] def add_builder(builder_class, &action) if builder_class.is_a?(String) or builder_class.is_a?(Symbol) - builder_class = BuilderBuilder.new(builder_class.to_s, Rscons::Builders::SimpleBuilder, &action) + name = builder_class.to_s + builder_class = BuilderBuilder.new(Rscons::Builders::SimpleBuilder, name, &action) + else + name = builder_class.name end - @builders[builder_class.name] = builder_class + @builders[name] = builder_class end # Add a build hook to the Environment. @@ -254,73 +258,29 @@ module Rscons # # @return [void] def process - cache = Cache.instance - unless cache.configuration_data["configured"] + unless Cache.instance.configuration_data["configured"] raise "Project must be configured before processing an Environment" end - failure = nil + @process_failure = nil + @process_blocking_wait = false + @process_commands_waiting_to_run = [] + @process_builder_waits = {} + @process_builders_to_run = [] begin - while @builder_set.size > 0 or @threaded_commands.size > 0 - - if failure + while @builder_set.size > 0 or @threads.size > 0 or @process_commands_waiting_to_run.size > 0 + process_step + if @process_failure + # On a build failure, do not start any more builders or commands, + # but let the threads that have already been started complete. @builder_set.clear - builder = nil - else - targets_still_building = @threaded_commands.map do |tc| - tc.builder.target - end - builder = @builder_set.get_next_builder_to_run(targets_still_building) + @process_commands_waiting_to_run.clear end - - # TODO: have Cache determine when checksums may be invalid based on - # file size and/or timestamp. - cache.clear_checksum_cache! - - if builder - result = run_builder(builder, cache) - unless result - failure = "Failed to build #{builder.target}" - Ansi.write($stderr, :red, failure, :reset, "\n") - next - end - end - - completed_tcs = Set.new - # First do a non-blocking wait to pick up any threads that have - # completed since last time. - while tc = wait_for_threaded_commands(nonblock: true) - completed_tcs << tc - end - - # If needed, do a blocking wait. - if (@threaded_commands.size > 0) and - ((completed_tcs.empty? and builder.nil?) or (@threaded_commands.size >= n_threads)) - completed_tcs << wait_for_threaded_commands - end - - # Process all completed {ThreadedCommand} objects. - completed_tcs.each do |tc| - result = finalize_builder(tc) - if result - @build_hooks[:post].each do |build_hook_block| - build_hook_block.call(tc.builder) - end - else - unless @echo == :command - print_failed_command(tc.command) - end - failure = "Failed to build #{tc.builder.target}" - Ansi.write($stderr, :red, failure, :reset, "\n") - break - end - end - end ensure - cache.write + Cache.instance.write end - if failure - raise BuildError.new(failure) + if @process_failure + raise BuildError.new(@process_failure) end end @@ -505,47 +465,6 @@ module Rscons end end - # Invoke a builder to build the given target based on the given sources. - # - # @param builder [Builder] The Builder to use. - # @param cache [Cache] The Cache. - # - # @return [String,false] Return value from the {Builder}'s +run+ method. - def run_builder(builder, cache) - builder.vars = @varset.merge(builder.vars) - build_operation = {} - call_build_hooks = lambda do |sec| - @build_hooks[sec].each do |build_hook_block| - build_hook_block.call(builder) - end - end - - # Invoke pre-build hooks. - call_build_hooks[:pre] - - # Call the builder's #run method. - rv = builder.run(build_operation) - - (@side_effects[builder.target] || []).each do |side_effect_file| - # Register side-effect files as build targets so that a Cache clean - # operation will remove them. - cache.register_build(side_effect_file, nil, [], self) - end - - if rv.is_a?(ThreadedCommand) - # Store the build operation so the post-build hooks can be called - # with it when the threaded command completes. - rv.builder = builder - # TODO: remove - rv.build_operation = build_operation - start_threaded_command(rv) - else - call_build_hooks[:post] if rv - end - - rv - end - # Expand a path to be relative to the Environment's build root. # # Paths beginning with "^/" are expanded by replacing "^" with the @@ -591,15 +510,6 @@ module Rscons end end - # Get the number of threads to use for parallelized builds in this - # Environment. - # - # @return [Integer] - # Number of threads to use for parallelized builds in this Environment. - def n_threads - @n_threads || Rscons.application.n_threads - end - # Print the builder run message, depending on the Environment's echo mode. # # @param short_description [String] @@ -636,49 +546,146 @@ module Rscons private - # Start a threaded command in a new thread. + # Signal a build failure to the {#process} method. # - # @param tc [ThreadedCommand] - # The ThreadedCommand to start. + # @param target [String] + # Build target name. # # @return [void] - def start_threaded_command(tc) - print_builder_run_message(tc.short_description, tc.command) - - env_args = tc.system_env ? [tc.system_env] : [] - options_args = tc.system_options ? [tc.system_options] : [] - system_args = [*env_args, *Rscons.command_executer, *tc.command, *options_args] - - tc.thread = Thread.new do - system(*system_args) - end - @threaded_commands << tc + def process_failure(target) + @process_failure = "Failed to build #{target}" + Ansi.write($stderr, :red, @process_failure, :reset, "\n") end - # Wait for threaded commands to complete. + # Run a builder and process its return value. # - # @param options [Hash] - # Options. - # @option options [Boolean] :nonblock - # Set to true to not block. + # @param builder [Builder] + # The builder. # - # @return [ThreadedCommand, nil] - # The {ThreadedCommand} object that is finished. - def wait_for_threaded_commands(options = {}) - threads = @threaded_commands.map(&:thread) - if finished_thread = find_finished_thread(threads, options[:nonblock]) - threaded_command = @threaded_commands.find do |tc| - tc.thread == finished_thread + # @return [void] + def run_builder(builder) + # TODO: have Cache determine when checksums may be invalid based on + # file size and/or timestamp. + Cache.instance.clear_checksum_cache! + case result = builder.run({}) + when Array + result.each do |waititem| + @process_builder_waits[builder] ||= Set.new + @process_builder_waits[builder] << waititem + case waititem + when Thread + @threads[waititem] = builder + when Command + @process_commands_waiting_to_run << waititem + when Builder + # No action needed. + else + raise "Unrecognized #{builder.name} builder return item: #{waititem.inspect}" + end end - @threaded_commands.delete(threaded_command) - threaded_command + when false + process_failure(builder.target) + when true + # Register side-effect files as build targets so that a Cache + # clean operation will remove them. + (@side_effects[builder.target] || []).each do |side_effect_file| + Cache.instance.register_build(side_effect_file, nil, [], self) + end + @build_hooks[:post].each do |build_hook_block| + build_hook_block.call(builder) + end + process_remove_wait(builder) + else + raise "Unrecognized #{builder.name} builder return value: #{result.inspect}" end end - # Check if any of the requested threads are finished. + # Remove an item that a builder may have been waiting on. + # + # @param waititem [Object] + # Item that a builder may be waiting on. + # + # @return [void] + def process_remove_wait(waititem) + @process_builder_waits.to_a.each do |builder, waits| + if waits.include?(waititem) + waits.delete(waititem) + end + if waits.empty? + @process_builder_waits.delete(builder) + @process_builders_to_run << builder + end + end + end + + # Broken out from {#process} to perform a single operation. + # + # @return [void] + def process_step + # Check if a thread has completed since last time. + thread = find_finished_thread(true) + + # Check if we need to do a blocking wait for a thread to complete. + if thread.nil? and (@threads.size >= n_threads or @process_blocking_wait) + thread = find_finished_thread(false) + @process_blocking_wait = false + end + + if thread + # We found a completed thread. + process_remove_wait(thread) + builder = builder_for_thread(thread) + completed_command = @threads[thread] + @threads.delete(thread) + if completed_command.is_a?(Command) + process_remove_wait(completed_command) + completed_command.status = thread.value + unless completed_command.status + unless @echo == :command + print_failed_command(completed_command.command) + end + return process_failure(builder.target) + end + end + end + + if @threads.size < n_threads and @process_commands_waiting_to_run.size > 0 + # There is a command waiting to run and a thread free to run it. + command = @process_commands_waiting_to_run.slice!(0) + @threads[command.run] = command + return + end + + unless @process_builders_to_run.empty? + # There is a builder waiting to run that was unblocked by its wait + # items completing. + return run_builder(@process_builders_to_run.slice!(0)) + end + + # If no builder was found to run yet and there are threads available, try + # to get a runnable builder from the builder set. + targets_still_building = @threads.reduce([]) do |result, (thread, obj)| + result << builder_for_thread(thread).target + end + builder = @builder_set.get_next_builder_to_run(targets_still_building) + + if builder + builder.vars = @varset.merge(builder.vars) + @build_hooks[:pre].each do |build_hook_block| + build_hook_block.call(builder) + end + return run_builder(builder) + end + + if @threads.size > 0 + # A runnable builder was not found but there is a thread running, + # so next time do a blocking wait for a thread to complete. + @process_blocking_wait = true + end + end + + # Find a finished thread. # - # @param threads [Array] - # The threads to check. # @param nonblock [Boolean] # Whether to be non-blocking. If true, nil will be returned if no thread # is finished. If false, the method will wait until one of the threads @@ -686,31 +693,32 @@ module Rscons # # @return [Thread, nil] # The finished thread, if any. - def find_finished_thread(threads, nonblock) + def find_finished_thread(nonblock) if nonblock - threads.find do |thread| + @threads.keys.find do |thread| !thread.alive? end else - if threads.empty? + if @threads.empty? raise "No threads to wait for" end - ThreadsWait.new(*threads).next_wait + ThreadsWait.new(*@threads.keys).next_wait end end - # Call a builder's #finalize method after a ThreadedCommand terminates. + # Get the {Builder} waiting on the given Thread. # - # @param tc [ThreadedCommand] - # The ThreadedCommand returned from the builder's #run method. + # @param thread [Thread] + # The thread. # - # @return [String, false] - # Result of Builder#finalize. - def finalize_builder(tc) - tc.builder.finalize( - tc.build_operation.merge( - command_status: tc.thread.value, - tc: tc)) + # @return [Builder] + # The {Builder} waiting on the given thread. + def builder_for_thread(thread) + if @threads[thread].is_a?(Command) + @threads[thread].builder + else + @threads[thread] + end end # Find a builder that meets the requested features and produces a target diff --git a/lib/rscons/threaded_command.rb b/lib/rscons/threaded_command.rb deleted file mode 100644 index 4e3cbbd..0000000 --- a/lib/rscons/threaded_command.rb +++ /dev/null @@ -1,61 +0,0 @@ -module Rscons - # If a builder returns an instance of this class from its #run method, then - # Rscons will execute the command specified in a thread and allow other - # builders to continue executing in parallel. - class ThreadedCommand - - # @return [Array] - # The command to execute. - attr_reader :command - - # @return [String] - # Short description of the command. This will be printed to standard - # output if the Environment's echo mode is :short. - attr_reader :short_description - - # @return [Hash] - # Environment Hash to pass to Kernel#system. - attr_reader :system_env - - # @return [Hash] - # Options Hash to pass to Kernel#system. - attr_reader :system_options - - # @return [Hash] - # Field for Rscons to store the build operation while this threaded - # command is executing. - attr_accessor :build_operation - - # @return [Builder] - # {Builder} executing this command. - attr_accessor :builder - - # @return [Thread] - # The thread waiting on this command to terminate. - attr_accessor :thread - - # Create a ThreadedCommand object. - # - # @param command [Array] - # The command to execute. - # @param options [Hash] - # Optional parameters. - # @option options [Object] :builder_info - # Arbitrary object to store builder-specific info. This object value will - # be passed back into the builder's #finalize method. - # @option options [String] :short_description - # Short description of the command. This will be printed to standard - # output if the Environment's echo mode is :short. - # @option options [Hash] :system_env - # Environment Hash to pass to Kernel#system. - # @option options [Hash] :system_options - # Options Hash to pass to Kernel#system. - def initialize(command, options = {}) - @command = command - @short_description = options[:short_description] - @system_env = options[:system_env] - @system_options = options[:system_options] - end - - end -end diff --git a/spec/build_tests_spec.rb b/spec/build_tests_spec.rb index 1c8371a..6900663 100644 --- a/spec/build_tests_spec.rb +++ b/spec/build_tests_spec.rb @@ -327,6 +327,29 @@ EOF expect(`./program.exe`).to eq "The value is 42\n" end + it 'raises an error when a custom builder returns an invalid value from #run' do + test_dir("custom_builder") + result = run_rscons(rsconscript: "error_run_return_value.rb") + expect(result.stderr).to match /Unrecognized MyBuilder builder return value: "hi"/ + expect(result.status).to_not eq 0 + end + + it 'raises an error when a custom builder returns an invalid value using Builder#wait_for' do + test_dir("custom_builder") + result = run_rscons(rsconscript: "error_wait_for.rb") + expect(result.stderr).to match /Unrecognized MyBuilder builder return item: 1/ + expect(result.status).to_not eq 0 + end + + it 'supports a Builder waiting for a custom Thread object' do + test_dir "custom_builder" + result = run_rscons(rsconscript: "wait_for_thread.rb") + expect(result.stderr).to eq "" + expect(result.status).to eq 0 + expect(lines(result.stdout)).to include "MyBuilder foo" + expect(File.exists?("foo")).to be_truthy + end + it 'allows cloning Environment objects' do test_dir('clone_env') result = run_rscons diff --git a/spec/rscons/builders/simple_builder_spec.rb b/spec/rscons/builders/simple_builder_spec.rb deleted file mode 100644 index fefff78..0000000 --- a/spec/rscons/builders/simple_builder_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -module Rscons - module Builders - describe SimpleBuilder do - let(:env) {Environment.new} - - it "should create a new builder with the given action" do - builder = Rscons::Builders::SimpleBuilder.new({}) { 0x1234 } - expect(builder.run(1,2,3,4,5)).to eq(0x1234) - end - end - end -end diff --git a/spec/rscons/environment_spec.rb b/spec/rscons/environment_spec.rb index 60098ec..34cefd1 100644 --- a/spec/rscons/environment_spec.rb +++ b/spec/rscons/environment_spec.rb @@ -230,7 +230,7 @@ module Rscons describe "#find_finished_thread" do it "raises an error if called with nonblock=false and no threads to wait for" do env = Environment.new - expect {env.__send__(:find_finished_thread, [], false)}.to raise_error /No threads to wait for/ + expect {env.__send__(:find_finished_thread, false)}.to raise_error /No threads to wait for/ end end