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.
This commit is contained in:
Josh Holtrop 2019-02-17 22:08:39 -05:00
parent 8426a54a57
commit b882f8de99
38 changed files with 580 additions and 665 deletions

View File

@ -5,7 +5,7 @@ class MySource < Rscons::Builder
#define THE_VALUE 5678
EOF
end
@target
true
end
end

View File

@ -5,7 +5,7 @@ class MySource < Rscons::Builder
#define THE_VALUE 678
EOF
end
@target
true
end
end

View File

@ -5,7 +5,7 @@ class MySource < Rscons::Builder
#define THE_VALUE #{@env.expand_varref("${the_value}")}
EOF
end
@target
true
end
end

View File

@ -0,0 +1,8 @@
build do
Environment.new do |env|
env.add_builder(:MyBuilder) do |options|
"hi"
end
env.MyBuilder("foo")
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
class TestBuilder < Rscons::Builder
def run(options)
target
true
end
end
build do

View File

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

View File

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

View File

@ -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")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>] :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<String>]
# The command to execute to build the target.
# @param sources [Array<String>] 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<String>]
# The command to execute.
# @param options [Hash]
# Options.
# @option options [Array<String>] :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, Command, Thread>]
# Builder(s) or Command(s) or Thread(s) that this builder needs to wait
# for.
def wait_for(things)
Array(things)
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String>] :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

View File

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

View File

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

View File

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

View File

@ -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<String>] :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

View File

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

View File

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

66
lib/rscons/command.rb Normal file
View File

@ -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<String>]
# 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<String>]
# 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

View File

@ -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<Thread>]
# 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

View File

@ -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<String>]
# 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<String>]
# 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

View File

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

View File

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

View File

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