557 lines
18 KiB
Ruby
557 lines
18 KiB
Ruby
require "fileutils"
|
|
require "set"
|
|
require "shellwords"
|
|
|
|
module Rscons
|
|
# The Environment class is the main programmatic interface to Rscons. It
|
|
# contains a collection of construction variables, options, builders, and
|
|
# rules for building targets.
|
|
class Environment
|
|
# Hash of +{"builder_name" => builder_object}+ pairs.
|
|
attr_reader :builders
|
|
|
|
# :command, :short, or :off
|
|
attr_accessor :echo
|
|
|
|
# String or +nil+
|
|
attr_reader :build_root
|
|
def build_root=(build_root)
|
|
@build_root = build_root
|
|
@build_root.gsub!('\\', '/') if @build_root
|
|
end
|
|
|
|
# Create an Environment object.
|
|
# @param options [Hash]
|
|
# Possible options keys:
|
|
# :echo => :command, :short, or :off (default :short)
|
|
# :build_root => String specifying build root directory (default nil)
|
|
# :exclude_builders => true to omit adding default builders (default false)
|
|
# If a block is given, the Environment object is yielded to the block and
|
|
# when the block returns, the {#process} method is automatically called.
|
|
def initialize(options = {})
|
|
@varset = VarSet.new
|
|
@targets = {}
|
|
@user_deps = {}
|
|
@builders = {}
|
|
@build_dirs = []
|
|
@build_hooks = []
|
|
unless options[:exclude_builders]
|
|
DEFAULT_BUILDERS.each do |builder_class_name|
|
|
builder_class = Builders.const_get(builder_class_name)
|
|
builder_class or raise "Could not find builder class #{builder_class_name}"
|
|
add_builder(builder_class.new)
|
|
end
|
|
end
|
|
@echo = options[:echo] || :short
|
|
@build_root = options[:build_root]
|
|
|
|
if block_given?
|
|
yield self
|
|
self.process
|
|
end
|
|
end
|
|
|
|
# Make a copy of the Environment object.
|
|
#
|
|
# By default, a cloned environment will contain a copy of all environment
|
|
# options, construction variables, and builders, but not a copy of the
|
|
# targets, build hooks, build directories, or the build root.
|
|
#
|
|
# Exactly which items are cloned are controllable via the optional :clone
|
|
# parameter, which can be :none, :all, or a set or array of any of the
|
|
# following:
|
|
# - :variables to clone construction variables (on by default)
|
|
# - :builders to clone the builders (on by default)
|
|
# - :build_root to clone the build root (off by default)
|
|
# - :build_dirs to clone the build directories (off by default)
|
|
# - :build_hooks to clone the build hooks (off by default)
|
|
#
|
|
# If a block is given, the Environment object is yielded to the block and
|
|
# when the block returns, the {#process} method is automatically called.
|
|
#
|
|
# Any options that #initialize receives can also be specified here.
|
|
#
|
|
# @return a new {Environment} object.
|
|
def clone(options = {})
|
|
clone = options[:clone] || Set[:variables, :builders]
|
|
clone = Set[:variables, :builders, :build_root, :build_dirs, :build_hooks] if clone == :all
|
|
clone = Set[] if clone == :none
|
|
clone = Set.new(clone) if clone.is_a?(Array)
|
|
clone.delete(:builders) if options[:exclude_builders]
|
|
env = self.class.new(
|
|
echo: options[:echo] || @echo,
|
|
build_root: options[:build_root],
|
|
exclude_builders: true)
|
|
if clone.include?(:builders)
|
|
@builders.each do |builder_name, builder|
|
|
env.add_builder(builder)
|
|
end
|
|
end
|
|
env.append(@varset) if clone.include?(:variables)
|
|
env.build_root = @build_root if clone.include?(:build_root)
|
|
if clone.include?(:build_dirs)
|
|
@build_dirs.each do |src_dir, obj_dir|
|
|
env.build_dir(src_dir, obj_dir)
|
|
end
|
|
end
|
|
if clone.include?(:build_hooks)
|
|
@build_hooks.each do |build_hook_block|
|
|
env.add_build_hook(&build_hook_block)
|
|
end
|
|
end
|
|
|
|
if block_given?
|
|
yield env
|
|
env.process
|
|
end
|
|
env
|
|
end
|
|
|
|
# Add a {Builder} object to the Environment.
|
|
def add_builder(builder)
|
|
@builders[builder.name] = builder
|
|
var_defs = builder.default_variables(self)
|
|
if var_defs
|
|
var_defs.each_pair do |var, val|
|
|
@varset[var] ||= val
|
|
end
|
|
end
|
|
end
|
|
|
|
# Add a build hook to the Environment.
|
|
def add_build_hook(&block)
|
|
@build_hooks << block
|
|
end
|
|
|
|
# Specify a build directory for this Environment.
|
|
# Source files from src_dir will produce object files under obj_dir.
|
|
def build_dir(src_dir, obj_dir)
|
|
src_dir = src_dir.gsub('\\', '/') if src_dir.is_a?(String)
|
|
@build_dirs << [src_dir, obj_dir]
|
|
end
|
|
|
|
# Return the file name to be built from source_fname with suffix suffix.
|
|
# This method takes into account the Environment's build directories.
|
|
def get_build_fname(source_fname, suffix)
|
|
build_fname = Rscons.set_suffix(source_fname, suffix).gsub('\\', '/')
|
|
found_match = @build_dirs.find do |src_dir, obj_dir|
|
|
if src_dir.is_a?(Regexp)
|
|
build_fname.sub!(src_dir, obj_dir)
|
|
else
|
|
build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/")
|
|
end
|
|
end
|
|
if @build_root and not found_match
|
|
unless Rscons.absolute_path?(source_fname) or build_fname.start_with?("#{@build_root}/")
|
|
build_fname = "#{@build_root}/#{build_fname}"
|
|
end
|
|
end
|
|
build_fname.gsub!('\\', '/')
|
|
build_fname
|
|
end
|
|
|
|
# Access a construction variable or environment option.
|
|
# @see VarSet#[]
|
|
def [](*args)
|
|
@varset.send(:[], *args)
|
|
end
|
|
|
|
# Set a construction variable or environment option.
|
|
# @see VarSet#[]=
|
|
def []=(*args)
|
|
@varset.send(:[]=, *args)
|
|
end
|
|
|
|
# Add a set of construction variables or environment options.
|
|
# @see VarSet#append
|
|
def append(*args)
|
|
@varset.append(*args)
|
|
end
|
|
|
|
# Build all target specified in the Environment.
|
|
# When a block is passed to Environment.new, this method is automatically
|
|
# called after the block returns.
|
|
def process
|
|
unless @targets.empty?
|
|
expand_paths!
|
|
cache = Cache.instance
|
|
cache.clear_checksum_cache!
|
|
targets_processed = {}
|
|
process_target = proc do |target|
|
|
targets_processed[target] ||= begin
|
|
@targets[target][:sources].each do |src|
|
|
if @targets.include?(src) and not targets_processed.include?(src)
|
|
process_target.call(src)
|
|
end
|
|
end
|
|
result = run_builder(@targets[target][:builder],
|
|
target,
|
|
@targets[target][:sources],
|
|
cache,
|
|
@targets[target][:vars] || {})
|
|
unless result
|
|
raise BuildError.new("Failed to build #{target}")
|
|
end
|
|
result
|
|
end
|
|
end
|
|
begin
|
|
@targets.each do |target, target_params|
|
|
process_target.call(target)
|
|
end
|
|
ensure
|
|
cache.write
|
|
end
|
|
end
|
|
end
|
|
|
|
# Clear all targets registered for the Environment.
|
|
def clear_targets
|
|
@targets = {}
|
|
end
|
|
|
|
# Expand a construction variable reference.
|
|
#
|
|
# @param varref [Array, String] Variable reference to expand.
|
|
# @param extra_vars [Hash, VarSet]
|
|
# Extra variables to use in addition to (or replace) the Environment's
|
|
# construction variables when expanding the variable reference.
|
|
#
|
|
# @return [Array, String] Expansion of the variable reference.
|
|
def expand_varref(varref, extra_vars = nil)
|
|
if extra_vars.nil?
|
|
@varset
|
|
else
|
|
@varset.merge(extra_vars)
|
|
end.expand_varref(varref)
|
|
end
|
|
alias_method :build_command, :expand_varref
|
|
|
|
# Execute a builder command
|
|
# @param short_desc [String] Message to print if the Environment's echo
|
|
# mode is set to :short
|
|
# @param command [Array] The command to execute.
|
|
# @param options [Hash] Optional options, possible keys:
|
|
# - :env - environment Hash to pass to Kernel#system.
|
|
# - :options - options Hash to pass to Kernel#system.
|
|
def execute(short_desc, command, options = {})
|
|
print_command = proc do
|
|
puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
|
|
end
|
|
if @echo == :command
|
|
print_command.call
|
|
elsif @echo == :short
|
|
puts short_desc
|
|
end
|
|
env_args = options[:env] ? [options[:env]] : []
|
|
options_args = options[:options] ? [options[:options]] : []
|
|
system(*env_args, *Rscons.command_executer, *command, *options_args).tap do |result|
|
|
unless result or @echo == :command
|
|
$stdout.write "Failed command was: "
|
|
print_command.call
|
|
end
|
|
end
|
|
end
|
|
|
|
def method_missing(method, *args)
|
|
if @builders.has_key?(method.to_s)
|
|
target, sources, vars, *rest = args
|
|
unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet)
|
|
raise "Unexpected construction variable set: #{vars.inspect}"
|
|
end
|
|
sources = Array(sources)
|
|
builder = @builders[method.to_s]
|
|
build_target = builder.create_build_target(env: self, target: target, sources: sources)
|
|
add_target(build_target.to_s, builder, sources, vars, rest)
|
|
build_target
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def add_target(target, builder, sources, vars, args)
|
|
@targets[target] = {
|
|
builder: builder,
|
|
sources: sources,
|
|
vars: vars,
|
|
args: args,
|
|
}
|
|
end
|
|
|
|
# Manually record a given target as depending on the specified files.
|
|
#
|
|
# @param target [String,BuildTarget] Target file.
|
|
# @param user_deps [Array<String>] Dependency files.
|
|
#
|
|
# @return [void]
|
|
def depends(target, *user_deps)
|
|
target = target.to_s
|
|
@user_deps[target] ||= []
|
|
@user_deps[target] = (@user_deps[target] + user_deps).uniq
|
|
end
|
|
|
|
# Return the list of user dependencies for a given target, or +nil+ for
|
|
# none.
|
|
def get_user_deps(target)
|
|
@user_deps[target]
|
|
end
|
|
|
|
# Build a list of source files into files containing one of the suffixes
|
|
# given by suffixes.
|
|
# This method is used internally by Rscons builders.
|
|
# @param sources [Array] List of source files to build.
|
|
# @param suffixes [Array] List of suffixes to try to convert source files into.
|
|
# @param cache [Cache] The Cache.
|
|
# @param vars [Hash] Extra variables to pass to the builder.
|
|
# Return a list of the converted file names.
|
|
def build_sources(sources, suffixes, cache, vars)
|
|
sources.map do |source|
|
|
if source.end_with?(*suffixes)
|
|
source
|
|
else
|
|
converted = nil
|
|
suffixes.each do |suffix|
|
|
converted_fname = get_build_fname(source, suffix)
|
|
builder = @builders.values.find { |b| b.produces?(converted_fname, source, self) }
|
|
if builder
|
|
converted = run_builder(builder, converted_fname, [source], cache, vars)
|
|
return nil unless converted
|
|
break
|
|
end
|
|
end
|
|
converted or raise "Could not find a builder to handle #{source.inspect}."
|
|
end
|
|
end
|
|
end
|
|
|
|
# Invoke a builder to build the given target based on the given sources.
|
|
# @param builder [Builder] The Builder to use.
|
|
# @param target [String] The target output file.
|
|
# @param sources [Array] List of source files.
|
|
# @param cache [Cache] The Cache.
|
|
# @param vars [Hash] Extra variables to pass to the builder.
|
|
# Return the result of the builder's run() method.
|
|
def run_builder(builder, target, sources, cache, vars)
|
|
vars = @varset.merge(vars)
|
|
@build_hooks.each do |build_hook_block|
|
|
build_operation = {
|
|
builder: builder,
|
|
target: target,
|
|
sources: sources,
|
|
vars: vars,
|
|
env: self,
|
|
}
|
|
build_hook_block.call(build_operation)
|
|
end
|
|
builder.run(target, sources, cache, self, vars)
|
|
end
|
|
|
|
# Expand a path to be relative to the Environment's build root.
|
|
#
|
|
# Paths beginning with "^/" are expanded by replacing "^" with the
|
|
# Environment's build root.
|
|
#
|
|
# @param path [String] The path to expand.
|
|
#
|
|
# @return [String] The expanded path.
|
|
def expand_path(path)
|
|
path.sub(%r{^\^(?=[\\/])}, @build_root)
|
|
end
|
|
|
|
# Execute a command using the system shell.
|
|
#
|
|
# The shell is automatically determined but can be overridden by the SHELL
|
|
# construction variable. If the SHELL construction variable is specified,
|
|
# the flag to pass to the shell is automatically dtermined but can be
|
|
# overridden by the SHELLFLAG construction variable.
|
|
#
|
|
# @param command [String] Command to execute.
|
|
#
|
|
# @return [String] The command's standard output.
|
|
def shell(command)
|
|
shell_cmd =
|
|
if self["SHELL"]
|
|
flag = self["SHELLFLAG"] || (self["SHELL"] == "cmd" ? "/c" : "-c")
|
|
[self["SHELL"], flag]
|
|
else
|
|
Rscons.get_system_shell
|
|
end
|
|
IO.popen([*shell_cmd, command]) do |io|
|
|
io.read
|
|
end
|
|
end
|
|
|
|
# @!method parse_flags(flags)
|
|
# @!method parse_flags!(flags)
|
|
#
|
|
# Parse command-line flags for compilation/linking options into separate
|
|
# construction variables.
|
|
#
|
|
# The parsed construction variables are returned in a Hash instead of
|
|
# merging them directly to the Environment. They can be merged with
|
|
# {#merge_flags}. The {#parse_flags!} version immediately merges the parsed
|
|
# flags as well.
|
|
#
|
|
# Example:
|
|
# # Import FreeType build options
|
|
# env.parse_flags!("!freetype-config --cflags --libs")
|
|
#
|
|
# @param flags [String]
|
|
# String containing the flags to parse, or if the flags string begins
|
|
# with "!", a shell command to execute using {#shell} to obtain the
|
|
# flags to parse.
|
|
#
|
|
# @return [Hash] Set of construction variables to append.
|
|
def parse_flags(flags)
|
|
if flags =~ /^!(.*)$/
|
|
flags = shell($1)
|
|
end
|
|
rv = {}
|
|
words = Shellwords.split(flags)
|
|
skip = false
|
|
words.each_with_index do |word, i|
|
|
if skip
|
|
skip = false
|
|
next
|
|
end
|
|
append = lambda do |var, val|
|
|
rv[var] ||= []
|
|
rv[var] += val
|
|
end
|
|
handle = lambda do |var, val|
|
|
if val.nil? or val.empty?
|
|
val = words[i + 1]
|
|
skip = true
|
|
end
|
|
if val and not val.empty?
|
|
append[var, [val]]
|
|
end
|
|
end
|
|
if word == "-arch"
|
|
if val = words[i + 1]
|
|
append["CCFLAGS", ["-arch", val]]
|
|
append["LDFLAGS", ["-arch", val]]
|
|
end
|
|
skip = true
|
|
elsif word =~ /^#{self["CPPDEFPREFIX"]}(.*)$/
|
|
handle["CPPDEFINES", $1]
|
|
elsif word == "-include"
|
|
if val = words[i + 1]
|
|
append["CCFLAGS", ["-include", val]]
|
|
end
|
|
skip = true
|
|
elsif word == "-isysroot"
|
|
if val = words[i + 1]
|
|
append["CCFLAGS", ["-isysroot", val]]
|
|
append["LDFLAGS", ["-isysroot", val]]
|
|
end
|
|
skip = true
|
|
elsif word =~ /^#{self["INCPREFIX"]}(.*)$/
|
|
handle["CPPPATH", $1]
|
|
elsif word =~ /^#{self["LIBLINKPREFIX"]}(.*)$/
|
|
handle["LIBS", $1]
|
|
elsif word =~ /^#{self["LIBDIRPREFIX"]}(.*)$/
|
|
handle["LIBPATH", $1]
|
|
elsif word == "-mno-cygwin"
|
|
append["CCFLAGS", [word]]
|
|
append["LDFLAGS", [word]]
|
|
elsif word == "-mwindows"
|
|
append["LDFLAGS", [word]]
|
|
elsif word == "-pthread"
|
|
append["CCFLAGS", [word]]
|
|
append["LDFLAGS", [word]]
|
|
elsif word =~ /^-std=/
|
|
append["CFLAGS", [word]]
|
|
elsif word =~ /^-Wa,(.*)$/
|
|
append["ASFLAGS", $1.split(",")]
|
|
elsif word =~ /^-Wl,(.*)$/
|
|
append["LDFLAGS", $1.split(",")]
|
|
elsif word =~ /^-Wp,(.*)$/
|
|
append["CPPFLAGS", $1.split(",")]
|
|
elsif word.start_with?("-")
|
|
append["CCFLAGS", [word]]
|
|
elsif word.start_with?("+")
|
|
append["CCFLAGS", [word]]
|
|
append["LDFLAGS", [word]]
|
|
else
|
|
append["LIBS", [word]]
|
|
end
|
|
end
|
|
rv
|
|
end
|
|
|
|
def parse_flags!(flags)
|
|
flags = parse_flags(flags)
|
|
merge_flags(flags)
|
|
flags
|
|
end
|
|
|
|
# Merge construction variable flags into this Environment's construction
|
|
# variables.
|
|
#
|
|
# This method does the same thing as {#append}, except that Array values in
|
|
# +flags+ are appended to the end of Array construction variables instead
|
|
# of replacing their contents.
|
|
#
|
|
# @param flags [Hash]
|
|
# Set of construction variables to merge into the current Environment.
|
|
# This can be the value (or a modified version) returned by
|
|
# {#parse_flags}.
|
|
#
|
|
# @return [void]
|
|
def merge_flags(flags)
|
|
flags.each_pair do |key, val|
|
|
if self[key].is_a?(Array) and val.is_a?(Array)
|
|
self[key] += val
|
|
else
|
|
self[key] = val
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
# Expand target and source paths before invoking builders.
|
|
#
|
|
# This method expand construction variable references in the target and
|
|
# source file names before passing them to the builder. It also expands
|
|
# "^/" prefixes to the Environment's build root if a build root is defined.
|
|
def expand_paths!
|
|
@targets = @targets.reduce({}) do |result, (target, target_params)|
|
|
sources = target_params[:sources].map do |source|
|
|
source = expand_path(source) if @build_root
|
|
expand_varref(source)
|
|
end
|
|
target = expand_path(target) if @build_root
|
|
target = expand_varref(target)
|
|
result[target] = target_params.merge(sources: sources)
|
|
result
|
|
end
|
|
end
|
|
|
|
# Parse dependencies for a given target from a Makefile.
|
|
# This method is used internally by Rscons builders.
|
|
# @param mf_fname [String] File name of the Makefile to read.
|
|
# @param target [String] Name of the target to gather dependencies for.
|
|
def self.parse_makefile_deps(mf_fname, target)
|
|
deps = []
|
|
buildup = ''
|
|
File.read(mf_fname).each_line do |line|
|
|
if line =~ /^(.*)\\\s*$/
|
|
buildup += ' ' + $1
|
|
else
|
|
buildup += ' ' + line
|
|
if buildup =~ /^(.*): (.*)$/
|
|
mf_target, mf_deps = $1.strip, $2
|
|
if mf_target == target
|
|
deps += mf_deps.split(' ').map(&:strip)
|
|
end
|
|
end
|
|
buildup = ''
|
|
end
|
|
end
|
|
deps
|
|
end
|
|
end
|
|
end
|