Compare commits

...
30 Commits
Author SHA1 Message Date
josh 03ffb39956 v1.4.0 2014-04-15 16:19:19 -04:00
josh 0a10af3f5e update README.md for v1.4.0 release notes 2014-04-15 16:18:58 -04:00
josh ddf00d08a7 shortcut all of Environment#process if @targets is empty 2014-04-15 16:18:33 -04:00
josh 8be1842cf8 update documentation for new builders 2014-04-15 16:06:00 -04:00
josh d424facf7f Add Preprocess and Disassemble builders 2014-04-15 15:21:55 -04:00
josh ee5aca275d Write Cache to disk if an exception occurs while processing the Environment - fix #4 2014-04-15 14:20:30 -04:00
josh ea30b0b5ee Fix Cache#mkdir_p to handle absolute paths - fix #5 2014-04-15 14:15:32 -04:00
josh 5a0d80b781 Cache: implement "dirty" attribute to only re-serialize when necessary 2014-04-15 14:11:10 -04:00
josh a2653a77d1 shortcut Environment#process if @targets is empty 2014-04-15 14:04:59 -04:00
josh 160434b903 Cache: change to a Singleton object so it is only loaded from disk once 2014-04-15 14:02:26 -04:00
josh 5ce8940d54 stop encoding Cache contents 2014-04-15 13:39:45 -04:00
josh 1a5e9ddaa6 Implement copy-on-write semantics for VarSet, speeding up Environment cloning 2014-04-15 11:21:15 -04:00
josh 5924154229 speed up VarSet#deep_dup a bit 2014-04-15 09:47:05 -04:00
josh 5beda54374 do not clone @varset when appending to the cloned environment since appending will do its own clone 2014-04-15 09:36:30 -04:00
josh ae0ad98075 use @varset.append() instead of @varset.send(:append) 2014-04-15 09:32:35 -04:00
josh f5be3a3571 VarSet: implement deep copying ourselves instead of using Marshal for a speed increase 2014-04-15 09:16:30 -04:00
josh fdc5c8773c Issue #7 - store MD5 of build command instead of the full command in the cache 2014-04-14 17:17:54 -04:00
josh c61380b354 Issue #7 - change .rsconscache from using YAML to JSON to speed up parsing 2014-04-14 16:52:21 -04:00
josh d8dda291a5 ensure that Dir[] results are predictable in build tests 2014-04-14 13:13:43 -04:00
josh 1fe2c6401d expand ^/ in target paths to be relative to the build root 2014-03-27 13:30:11 -04:00
josh 4818fd10fe README.md: add more details about Environment cloning 2014-03-27 12:02:46 -04:00
josh 5989e24ee6 update some documentation 2014-03-27 11:57:45 -04:00
josh 1875a2ea66 Pass the Environment object to build hooks in the :env key of the build_op parameter 2014-03-27 10:44:10 -04:00
josh cbd5816b4b add VarSet#include? 2014-03-27 10:39:46 -04:00
josh b2c970c9ca add Environment#add_target and use it from Environment#method_missing 2014-03-26 11:29:02 -04:00
josh 92a9982988 use #super from #method_missing instead of method aliasing 2014-03-26 11:23:32 -04:00
josh 533ee9c16c add CFile builder to create C/C++ sources from lex/yacc input files 2014-03-18 21:57:53 -04:00
josh 831b9bf2c7 v1.3.0 2014-02-25 12:18:02 -05:00
josh 031395bb57 add v1.3.0 release notes 2014-02-25 12:17:49 -05:00
josh e3aeb7f4d7 Change Environment#execute() options Hash to contain :env and :options keys for Kernel#system() 2014-02-25 12:14:52 -05:00
17 changed files with 717 additions and 245 deletions

No files matched your search

+119 -10
View File
@@ -12,7 +12,7 @@ Add this line to your application's Gemfile:
And then execute: And then execute:
$ bundle $ bundle install
Or install it yourself as: Or install it yourself as:
@@ -176,11 +176,14 @@ subset of build targets or source files.
The `build_op` parameter to the build hook block is a Hash describing the The `build_op` parameter to the build hook block is a Hash describing the
build operation with the following keys: build operation with the following keys:
* `:builder` - `Builder` instance in use * `:builder` - `Builder` instance in use
* `:env` - `Environment` calling the build hook; note that this may be
different from the Environment that the build hook was added to in the case
that the original Environment was cloned with build hooks!
* `:target` - `String` name of the target file * `:target` - `String` name of the target file
* `:sources` - `Array` of the source files * `:sources` - `Array` of the source files
* `:vars` - `Rscons::VarSet` containing the construction variables to use * `:vars` - `Rscons::VarSet` containing the construction variables to use.
The build hook can overwrite entries in `build_op[:vars]` to alter the The build hook can overwrite entries in `build_op[:vars]` to alter the
construction variables in use for this specific build operation. construction variables in use for this specific build operation.
### Example: Creating a static library ### Example: Creating a static library
@@ -190,19 +193,96 @@ Rscons::Environment.new do |env|
end end
``` ```
### Example: Creating a C++ parser source from a Yacc/Bison input file
```ruby
Rscons::Environment.new do |env|
env.CFile("#{env.build_root}/parser.tab.cc", "parser.yy")
end
```
## Details ## Details
### Default Builders ### Builders
Rscons ships with a number of default builders: Rscons ships with a number of builders:
* CFile, which builds a C or C++ source file from a lex or yacc input file
* Disassemble, which disassembles an object file to a disassembly listing
* Library, which collects object files into a static library archive file * Library, which collects object files into a static library archive file
* Object, which compiles source files to produce an object file * Object, which compiles source files to produce an object file
* Preprocess, which invokes the C/C++ preprocessor on a source file
* Program, which links object files to produce an executable * Program, which links object files to produce an executable
If you want to create an Environment that does not contain any builders, If you want to create an Environment that does not contain any builders,
you can use the `exclude_builders` key to the Environment constructor. you can use the `exclude_builders` key to the Environment constructor.
#### CFile
```ruby
env.CFile(target, source)
# Example
env.CFile("parser.c", "parser.y")
```
The CFile builder will generate a C or C++ source file from a lex (.l, .ll)
or yacc (.y, .yy) input file.
#### Disassemble
```ruby
env.Disassemble(target, source)
# Example
env.Disassemble("module.dis", "module.o")
```
The Disassemble builder generates a disassembly listing using objdump from
and object file.
#### Library
```ruby
env.Library(target, sources)
# Example
env.Library("lib.a", Dir["src/**/*.c"])
```
The Library builder creates a static library archive from the given source
files.
#### Object
```ruby
env.Object(target, sources)
# Example
env.Object("module.o", "module.c")
```
The Object builder compiles the given sources to an object file.
#### Preprocess
```ruby
env.Preprocess(target, source)
# Example
env.Preprocess("module-preprocessed.cc", "module.cc")
```
The Preprocess builder invokes either ${CC} or ${CXX} (depending on if the
source contains an extension in ${CXXSUFFIX} or not) and writes the
preprocessed output to the target file.
#### Program
```ruby
env.Program(target, sources)
# Example
env.Program("myprog", Dir["src/**/*.cc"])
```
The Program builder compiles and links the given sources to an executable file.
Object files or source files can be given as `sources`.
### Managing Environments ### Managing Environments
An Rscons::Environment consists of: An Rscons::Environment consists of:
@@ -214,10 +294,17 @@ An Rscons::Environment consists of:
* a collection of targets to build * a collection of targets to build
* a collection of build hooks * a collection of build hooks
When cloning an environment, the construction variables and builders are When cloning an environment, by default the construction variables and builders
cloned, but the new environment does not inherit any of the targets, build are cloned, but the new environment does not inherit any of the targets, build
hooks, build directories, or the build root from the source environment. hooks, build directories, or the build root from the source environment.
The set of environment attributes that are cloned is controllable via the
`:clone` option to the `#clone` method.
For example, `env.clone(clone: :all)` will include construction variables,
builders, build hooks, build directories, and the build root.
The set of pending targets is never cloned.
Cloned environments contain "deep copies" of construction variables. Cloned environments contain "deep copies" of construction variables.
For example, in: For example, in:
@@ -233,8 +320,7 @@ cloned_env["CPPPATH"] << "three"
### Construction Variable Naming ### Construction Variable Naming
* uppercase strings - the default construction variables that Rscons uses * uppercase strings - the default construction variables that Rscons uses
* lowercase symbols - Rscons options * symbols, lowercase strings - reserved as user-defined construction variables
* lowercase strings - reserved as user-defined construction variables
### API documentation ### API documentation
@@ -243,7 +329,30 @@ http://rubydoc.info/github/holtrop/rscons/frames.
## Release Notes ## Release Notes
### v1.4.0
- add CFile builder
- add Disassemble builder
- add Preprocess builder
- pass the Environment object to build hooks in the :env key of the build_op parameter
- expand target/source paths beginning with "^/" to be relative to the Environment's build root
- many performance improvements, including:
- use JSON instead of YAML for the cache to improve loading speed (Issue #7)
- store a hash of the build command instead of the full command contents in the cache
- implement copy-on-write semantics for construction variables when cloning Environments
- only load the cache once instead of on each Environment#process
- only write the cache when something has changed
- fix Cache#mkdir_p to handle relative paths (Issue #5)
- flush the cache to disk if a builder raises an exception (Issue #4)
### v1.3.0
- change Environment#execute() options parameter to accept the following options keys:
- :env to pass an environment Hash to Kernel#system
- :options to pass an options Hash to Kernel#system
### v1.2.0 ### v1.2.0
- add :clone option to Environment#clone to control exactly which Environment attributes are cloned - add :clone option to Environment#clone to control exactly which Environment attributes are cloned
- allow nil to be passed in to Environment#build_root= - allow nil to be passed in to Environment#build_root=
+8 -2
View File
@@ -5,15 +5,21 @@ require_relative "rscons/varset"
require_relative "rscons/version" require_relative "rscons/version"
# default builders # default builders
require_relative "rscons/builders/cfile"
require_relative "rscons/builders/disassemble"
require_relative "rscons/builders/library" require_relative "rscons/builders/library"
require_relative "rscons/builders/object" require_relative "rscons/builders/object"
require_relative "rscons/builders/preprocess"
require_relative "rscons/builders/program" require_relative "rscons/builders/program"
# Namespace module for rscons classes # Namespace module for rscons classes
module Rscons module Rscons
DEFAULT_BUILDERS = [ DEFAULT_BUILDERS = [
:CFile,
:Disassemble,
:Library, :Library,
:Object, :Object,
:Preprocess,
:Program, :Program,
] ]
@@ -21,7 +27,7 @@ module Rscons
# Remove all generated files # Remove all generated files
def self.clean def self.clean
cache = Cache.new cache = Cache.instance
# remove all built files # remove all built files
cache.targets.each do |target| cache.targets.each do |target|
FileUtils.rm_f(target) FileUtils.rm_f(target)
@@ -33,7 +39,7 @@ module Rscons
Dir.rmdir(directory) rescue nil Dir.rmdir(directory) rescue nil
end end
end end
Cache.clear cache.clear
end end
# Return whether the given path is an absolute filesystem path or not # Return whether the given path is an absolute filesystem path or not
+40
View File
@@ -0,0 +1,40 @@
module Rscons
module Builders
# Build a C or C++ source file given a lex (.l, .ll) or yacc (.y, .yy)
# input file.
#
# Examples::
# env.CFile("parser.tab.cc", "parser.yy")
# env.CFile("lex.yy.cc", "parser.ll")
class CFile < Builder
def default_variables(env)
{
"YACC" => "bison",
"YACC_FLAGS" => ["-d"],
"YACC_CMD" => ["${YACC}", "${YACC_FLAGS}", "-o", "${_TARGET}", "${_SOURCES}"],
"LEX" => "flex",
"LEX_FLAGS" => [],
"LEX_CMD" => ["${LEX}", "${LEX_FLAGS}", "-o", "${_TARGET}", "${_SOURCES}"],
}
end
def run(target, sources, cache, env, vars)
vars = vars.merge({
"_TARGET" => target,
"_SOURCES" => sources,
})
cmd =
case
when sources.first.end_with?(".l", ".ll")
"LEX"
when sources.first.end_with?(".y", ".yy")
"YACC"
else
raise "Unknown source file #{sources.first.inspect} for CFile builder"
end
command = env.build_command(env["#{cmd}_CMD"], vars)
standard_build("#{cmd} #{target}", target, command, sources, env, cache)
end
end
end
end
+25
View File
@@ -0,0 +1,25 @@
module Rscons
module Builders
# The Disassemble builder produces a disassembly listing of a source file.
class Disassemble < Builder
def default_variables(env)
{
"OBJDUMP" => "objdump",
"DISASM_CMD" => ["${OBJDUMP}", "${DISASM_FLAGS}", "${_SOURCES}"],
"DISASM_FLAGS" => ["--disassemble", "--source"],
}
end
def run(target, sources, cache, env, vars)
vars = vars.merge("_SOURCES" => sources)
command = env.build_command(env["DISASM_CMD"], vars)
unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target))
return false unless env.execute("Disassemble #{target}", command, options: {out: target})
cache.register_build(target, command, sources, env)
end
target
end
end
end
end
+1 -1
View File
@@ -1,7 +1,7 @@
module Rscons module Rscons
module Builders module Builders
# A default Rscons builder that produces a static library archive. # A default Rscons builder that produces a static library archive.
class Rscons::Builders::Library < Rscons::Builder class Library < Builder
def default_variables(env) def default_variables(env)
{ {
'AR' => 'ar', 'AR' => 'ar',
+25
View File
@@ -0,0 +1,25 @@
module Rscons
module Builders
# The Preprocess builder invokes the C preprocessor
class Preprocess < Builder
def default_variables(env)
{
"CPP_CMD" => ["${_PREPROCESS_CC}", "-E", "-o", "${_TARGET}", "-I${CPPPATH}", "${CPPFLAGS}", "${CFLAGS}", "${_SOURCES}"],
}
end
def run(target, sources, cache, env, vars)
pp_cc = if sources.find {|s| s.end_with?(*env["CXXSUFFIX"])}
env["CXX"]
else
env["CC"]
end
vars = vars.merge("_PREPROCESS_CC" => pp_cc,
"_TARGET" => target,
"_SOURCES" => sources)
command = env.build_command(env["CPP_CMD"], vars)
standard_build("Preprocess #{target}", target, command, sources, env, cache)
end
end
end
end
+1 -1
View File
@@ -2,7 +2,7 @@ module Rscons
module Builders module Builders
# A default Rscons builder that knows how to link object files into an # A default Rscons builder that knows how to link object files into an
# executable program. # executable program.
class Rscons::Builders::Program < Rscons::Builder class Program < Builder
def default_variables(env) def default_variables(env)
{ {
'LD' => nil, 'LD' => nil,
+97 -81
View File
@@ -1,88 +1,87 @@
require 'yaml' require "digest/md5"
require 'fileutils' require "fileutils"
require 'digest/md5' require "json"
require 'set' require "set"
require 'rscons/version' require "singleton"
require "rscons/version"
module Rscons module Rscons
# The Cache class keeps track of file checksums, build target commands and # The Cache class keeps track of file checksums, build target commands and
# dependencies in a YAML file which persists from one invocation to the next. # dependencies in a JSON file which persists from one invocation to the next.
# Example cache: # Example cache:
# { # {
# version: '1.2.3', # "version" => "1.2.3",
# targets: { # "targets" => {
# 'program' => { # "program" => {
# 'checksum' => 'A1B2C3D4', # "checksum" => "A1B2C3D4",
# 'command' => ['gcc', '-o', 'program', 'program.o'], # "command" => "13543518FE",
# 'deps' => [ # "deps" => [
# { # {
# 'fname' => 'program.o', # "fname" => "program.o",
# 'checksum' => '87654321', # "checksum" => "87654321",
# }, # },
# ], # ],
# 'user_deps' => [ # "user_deps" => [
# { # {
# 'fname' => 'lscript.ld', # "fname" => "lscript.ld",
# 'checksum' => '77551133', # "checksum" => "77551133",
# }, # },
# ], # ],
# }, # },
# 'program.o' => { # "program.o" => {
# 'checksum' => '87654321', # "checksum" => "87654321",
# 'command' => ['gcc', '-c', '-o', 'program.o', 'program.c'], # "command" => "98765ABCD",
# 'deps' => [ # "deps" => [
# { # {
# 'fname' => 'program.c', # "fname" => "program.c",
# 'checksum' => '456789ABC', # "checksum" => "456789ABC",
# }, # },
# { # {
# 'fname' => 'program.h', # "fname" => "program.h",
# 'checksum' => '7979764643', # "checksum" => "7979764643",
# }, # },
# ], # ],
# 'user_deps' => [], # "user_deps" => [],
# } # }
# }, # },
# directories: { # "directories" => {
# 'build' => true, # "build" => true,
# 'build/one' => true, # "build/one" => true,
# 'build/two' => true, # "build/two" => true,
# }, # },
# } # }
class Cache class Cache
#### Constants include Singleton
# Name of the file to store cache information in # Name of the file to store cache information in
CACHE_FILE = '.rsconscache' CACHE_FILE = ".rsconscache"
#### Class Methods
# Remove the cache file
def self.clear
FileUtils.rm_f(CACHE_FILE)
end
#### Instance Methods
# Create a Cache object and load in the previous contents from the cache # Create a Cache object and load in the previous contents from the cache
# file. # file.
def initialize def initialize
@cache = YAML.load(File.read(CACHE_FILE)) rescue {} initialize!
unless @cache.is_a?(Hash) end
$stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
@cache = {} # Remove the cache file.
end def clear
@cache[:targets] ||= {} FileUtils.rm_f(CACHE_FILE)
@cache[:directories] ||= {} initialize!
end
# Clear the cached file checksums.
def clear_checksum_cache!
@lookup_checksums = {} @lookup_checksums = {}
end end
# Write the cache to disk to be loaded next time. # Write the cache to disk to be loaded next time.
def write def write
@cache[:version] = VERSION if @dirty || (@cache["version"] != VERSION)
File.open(CACHE_FILE, 'w') do |fh| @cache["version"] = VERSION
fh.puts(YAML.dump(@cache)) File.open(CACHE_FILE, "w") do |fh|
fh.puts(JSON.dump(@cache))
end
end end
@dirty = false
end end
# Check if target(s) are up to date # Check if target(s) are up to date
@@ -90,11 +89,12 @@ module Rscons
# @param command [String, Array] The command used to build the target. # @param command [String, Array] The command used to build the target.
# @param deps [Array] List of the target's dependency files. # @param deps [Array] List of the target's dependency files.
# @param env [Environment] The Rscons::Environment. # @param env [Environment] The Rscons::Environment.
# @param options [Hash] Optional options. Can contain the following keys: # @param options [Hash] Optional options.
# :strict_deps:: # @option options [Boolean] :strict_deps
# Only consider a target up to date if its list of dependencies is # Only consider a target up to date if its list of dependencies is
# exactly equal (including order) to the cached list of dependencies # exactly equal (including order) to the cached list of dependencies
# @return true if the targets are all up to date, meaning that, # @return [Boolean]
# True value if the targets are all up to date, meaning that,
# for each target: # for each target:
# - the target exists on disk # - the target exists on disk
# - the cache has information for the target # - the cache has information for the target
@@ -111,16 +111,16 @@ module Rscons
return false unless File.exists?(target) return false unless File.exists?(target)
# target must be registered in the cache # target must be registered in the cache
return false unless @cache[:targets].has_key?(target) return false unless @cache["targets"].has_key?(target)
# target must have the same checksum as when it was built last # target must have the same checksum as when it was built last
return false unless @cache[:targets][target][:checksum] == lookup_checksum(target) return false unless @cache["targets"][target]["checksum"] == lookup_checksum(target)
# command used to build target must be identical # command used to build target must be identical
return false unless @cache[:targets][target][:command] == command return false unless @cache["targets"][target]["command"] == Digest::MD5.hexdigest(command.inspect)
cached_deps = @cache[:targets][target][:deps] || [] cached_deps = @cache["targets"][target]["deps"] || []
cached_deps_fnames = cached_deps.map { |dc| dc[:fname] } cached_deps_fnames = cached_deps.map { |dc| dc["fname"] }
if options[:strict_deps] if options[:strict_deps]
# depedencies passed in must exactly equal those in the cache # depedencies passed in must exactly equal those in the cache
return false unless deps == cached_deps_fnames return false unless deps == cached_deps_fnames
@@ -131,13 +131,13 @@ module Rscons
# set of user dependencies must match # set of user dependencies must match
user_deps = env.get_user_deps(target) || [] user_deps = env.get_user_deps(target) || []
cached_user_deps = @cache[:targets][target][:user_deps] || [] cached_user_deps = @cache["targets"][target]["user_deps"] || []
cached_user_deps_fnames = cached_user_deps.map { |dc| dc[:fname] } cached_user_deps_fnames = cached_user_deps.map { |dc| dc["fname"] }
return false unless user_deps == cached_user_deps_fnames return false unless user_deps == cached_user_deps_fnames
# all cached dependencies must have their checksums match # all cached dependencies must have their checksums match
(cached_deps + cached_user_deps).each do |dep_cache| (cached_deps + cached_user_deps).each do |dep_cache|
return false unless dep_cache[:checksum] == lookup_checksum(dep_cache[:fname]) return false unless dep_cache["checksum"] == lookup_checksum(dep_cache["fname"])
end end
end end
@@ -148,54 +148,70 @@ module Rscons
# @param targets [String, Array] The name of the target(s) built. # @param targets [String, Array] The name of the target(s) built.
# @param command [String, Array] The command used to build the target. # @param command [String, Array] The command used to build the target.
# @param deps [Array] List of dependencies for the target. # @param deps [Array] List of dependencies for the target.
# @param env [Environment] The Rscons::Environment. # @param env [Environment] The {Rscons::Environment}.
def register_build(targets, command, deps, env) def register_build(targets, command, deps, env)
Array(targets).each do |target| Array(targets).each do |target|
@cache[:targets][target.encode(__ENCODING__)] = { @cache["targets"][target] = {
command: command, "command" => Digest::MD5.hexdigest(command.inspect),
checksum: calculate_checksum(target), "checksum" => calculate_checksum(target),
deps: deps.map do |dep| "deps" => deps.map do |dep|
{ {
fname: dep.encode(__ENCODING__), "fname" => dep,
checksum: lookup_checksum(dep), "checksum" => lookup_checksum(dep),
} }
end, end,
user_deps: (env.get_user_deps(target) || []).map do |dep| "user_deps" => (env.get_user_deps(target) || []).map do |dep|
{ {
fname: dep.encode(__ENCODING__), "fname" => dep,
checksum: lookup_checksum(dep), "checksum" => lookup_checksum(dep),
} }
end, end,
} }
@dirty = true
end end
end end
# Return a list of targets that have been built # Return a list of targets that have been built
def targets def targets
@cache[:targets].keys @cache["targets"].keys
end end
# Make any needed directories and record the ones that are created for # Make any needed directories and record the ones that are created for
# removal upon a "clean" operation. # removal upon a "clean" operation.
def mkdir_p(path) def mkdir_p(path)
parts = path.split(/[\\\/]/) parts = path.split(/[\\\/]/)
(0..parts.size-1).each do |i| parts.each_index do |i|
subpath = File.join(*parts[0, i + 1]).encode(__ENCODING__) next if parts[i] == ""
subpath = File.join(*parts[0, i + 1])
unless File.exists?(subpath) unless File.exists?(subpath)
FileUtils.mkdir(subpath) FileUtils.mkdir(subpath)
@cache[:directories][subpath] = true @cache["directories"][subpath] = true
@dirty = true
end end
end end
end end
# Return a list of directories which were created as a part of the build # Return a list of directories which were created as a part of the build
def directories def directories
@cache[:directories].keys @cache["directories"].keys
end end
# Private Instance Methods
private private
# Create a Cache object and load in the previous contents from the cache
# file.
def initialize!
@cache = JSON.load(File.read(CACHE_FILE)) rescue {}
unless @cache.is_a?(Hash)
$stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
@cache = {}
end
@cache["targets"] ||= {}
@cache["directories"] ||= {}
@lookup_checksums = {}
@dirty = false
end
# Return a file's checksum, or the previously calculated checksum for # Return a file's checksum, or the previously calculated checksum for
# the same file # the same file
# @param file [String] The file name. # @param file [String] The file name.
@@ -206,7 +222,7 @@ module Rscons
# Calculate and return a file's checksum # Calculate and return a file's checksum
# @param file [String] The file name. # @param file [String] The file name.
def calculate_checksum(file) def calculate_checksum(file)
@lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, mode: 'rb')).encode(__ENCODING__) rescue '' @lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, mode: "rb")) rescue ""
end end
end end
end end
+69 -34
View File
@@ -86,7 +86,7 @@ module Rscons
env.add_builder(builder) env.add_builder(builder)
end end
end end
env.append(@varset.clone) if clone.include?(:variables) env.append(@varset) if clone.include?(:variables)
env.build_root = @build_root if clone.include?(:build_root) env.build_root = @build_root if clone.include?(:build_root)
if clone.include?(:build_dirs) if clone.include?(:build_dirs)
@build_dirs.each do |src_dir, obj_dir| @build_dirs.each do |src_dir, obj_dir|
@@ -164,38 +164,44 @@ module Rscons
# Add a set of construction variables or environment options. # Add a set of construction variables or environment options.
# @see VarSet#append # @see VarSet#append
def append(*args) def append(*args)
@varset.send(:append, *args) @varset.append(*args)
end end
# Build all target specified in the Environment. # Build all target specified in the Environment.
# When a block is passed to Environment.new, this method is automatically # When a block is passed to Environment.new, this method is automatically
# called after the block returns. # called after the block returns.
def process def process
cache = Cache.new unless @targets.empty?
targets_processed = {} clean_target_paths!
process_target = proc do |target| cache = Cache.instance
targets_processed[target] ||= begin cache.clear_checksum_cache!
@targets[target][:source].each do |src| targets_processed = {}
if @targets.include?(src) and not targets_processed.include?(src) process_target = proc do |target|
process_target.call(src) 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 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
result = run_builder(@targets[target][:builder], end
target, begin
@targets[target][:source], @targets.each do |target, target_params|
cache, process_target.call(target)
@targets[target][:vars] || {})
unless result
cache.write
raise BuildError.new("Failed to build #{target}")
end end
result ensure
cache.write
end end
end end
@targets.each do |target, info|
process_target.call(target)
end
cache.write
end end
# Clear all targets registered for the Environment. # Clear all targets registered for the Environment.
@@ -224,7 +230,9 @@ module Rscons
# @param short_desc [String] Message to print if the Environment's echo # @param short_desc [String] Message to print if the Environment's echo
# mode is set to :short # mode is set to :short
# @param command [Array] The command to execute. # @param command [Array] The command to execute.
# @param options [Hash] Optional options to pass to Kernel#system. # @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 = {}) def execute(short_desc, command, options = {})
print_command = proc do print_command = proc do
puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ') puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
@@ -234,7 +242,9 @@ module Rscons
elsif @echo == :short elsif @echo == :short
puts short_desc puts short_desc
end end
system(*command, options).tap do |result| env_args = options[:env] ? [options[:env]] : []
options_args = options[:options] ? [options[:options]] : []
system(*env_args, *command, *options_args).tap do |result|
unless result or @echo == :command unless result or @echo == :command
$stdout.write "Failed command was: " $stdout.write "Failed command was: "
print_command.call print_command.call
@@ -242,25 +252,28 @@ module Rscons
end end
end end
alias_method :orig_method_missing, :method_missing
def method_missing(method, *args) def method_missing(method, *args)
if @builders.has_key?(method.to_s) if @builders.has_key?(method.to_s)
target, source, vars, *rest = args target, sources, vars, *rest = args
unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet) unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet)
raise "Unexpected construction variable set: #{vars.inspect}" raise "Unexpected construction variable set: #{vars.inspect}"
end end
source = [source] unless source.is_a?(Array) sources = [sources] unless sources.is_a?(Array)
@targets[target] = { add_target(target, @builders[method.to_s], sources, vars, rest)
builder: @builders[method.to_s],
source: source,
vars: vars,
args: rest,
}
else else
orig_method_missing(method, *args) super
end end
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 # Manually record a given target as depending on the specified
# dependency files. # dependency files.
def depends(target, *user_deps) def depends(target, *user_deps)
@@ -317,12 +330,34 @@ module Rscons
target: target, target: target,
sources: sources, sources: sources,
vars: vars, vars: vars,
env: self,
} }
build_hook_block.call(build_operation) build_hook_block.call(build_operation)
end end
builder.run(target, sources, cache, self, vars) builder.run(target, sources, cache, self, vars)
end end
private
# Expand all target paths that begin with ^/ to be relative to the
# Environment's build root, if present
def clean_target_paths!
if @build_root
expand = lambda do |path|
path.sub(%r{^\^(?=[\\/])}, @build_root)
end
new_targets = {}
@targets.each_pair do |target, target_params|
target_params[:sources].map! do |source|
expand[source]
end
new_targets[expand[target]] = target_params
end
@targets = new_targets
end
end
# Parse dependencies for a given target from a Makefile. # Parse dependencies for a given target from a Makefile.
# This method is used internally by Rscons builders. # This method is used internally by Rscons builders.
# @param mf_fname [String] File name of the Makefile to read. # @param mf_fname [String] File name of the Makefile to read.
+75 -18
View File
@@ -1,45 +1,73 @@
module Rscons module Rscons
# This class represents a collection of variables which can be accessed # This class represents a collection of variables which can be accessed
# as certain types # as certain types.
# Only nil, strings, arrays, and hashes should be stored in a VarSet.
class VarSet class VarSet
# The underlying hash # Create a VarSet.
attr_reader :vars
# Create a VarSet
# @param vars [Hash] Optional initial variables. # @param vars [Hash] Optional initial variables.
def initialize(vars = {}) def initialize(vars = {})
if vars.is_a?(VarSet) @my_vars = {}
@vars = vars.clone.vars @coa_vars = []
else append(vars)
@vars = vars
end
end end
# Access the value of variable as a particular type # Access the value of variable as a particular type
# @param key [String, Symbol] The variable name. # @param key [String, Symbol] The variable name.
# @return [Object] The variable's value.
def [](key) def [](key)
@vars[key] if @my_vars.include?(key)
@my_vars[key]
else
@coa_vars.each do |coa_vars|
if coa_vars.include?(key)
@my_vars[key] = deep_dup(coa_vars[key])
return @my_vars[key]
end
end
nil
end
end end
# Assign a value to a variable. # Assign a value to a variable.
# @param key [String, Symbol] The variable name. # @param key [String, Symbol] The variable name.
# @param val [Object] The value. # @param val [Object] The value to set.
def []=(key, val) def []=(key, val)
@vars[key] = val @my_vars[key] = val
end
# Check if the VarSet contains a variable.
# @param key [String, Symbol] The variable name.
# @return [true, false] Whether the VarSet contains a variable.
def include?(key)
if @my_vars.include?(key)
true
else
@coa_vars.find do |coa_vars|
coa_vars.include?(key)
end
end
end end
# Add or overwrite a set of variables # Add or overwrite a set of variables
# @param values [VarSet, Hash] New set of variables. # @param values [VarSet, Hash] New set of variables.
def append(values) def append(values)
values = values.vars if values.is_a?(VarSet) coa!
@vars.merge!(Marshal.load(Marshal.dump(values))) if values.is_a?(VarSet)
values.send(:coa!)
@coa_vars = values.instance_variable_get(:@coa_vars) + @coa_vars
else
@my_vars = deep_dup(values)
end
self self
end end
# Create a new VarSet object based on the first merged with other. # Create a new VarSet object based on the first merged with other.
# @param other [VarSet, Hash] Other variables to add or overwrite. # @param other [VarSet, Hash] Other variables to add or overwrite.
def merge(other = {}) def merge(other = {})
VarSet.new(Marshal.load(Marshal.dump(@vars))).append(other) coa!
varset = self.class.new
varset.instance_variable_set(:@coa_vars, @coa_vars.dup)
varset.append(other)
end end
alias_method :clone, :merge alias_method :clone, :merge
@@ -54,18 +82,47 @@ module Rscons
else else
if varref =~ /^(.*)\$\{([^}]+)\}(.*)$/ if varref =~ /^(.*)\$\{([^}]+)\}(.*)$/
prefix, varname, suffix = $1, $2, $3 prefix, varname, suffix = $1, $2, $3
varval = expand_varref(@vars[varname]) varval = expand_varref(self[varname])
if varval.is_a?(String) if varval.is_a?(String)
expand_varref("#{prefix}#{varval}#{suffix}") expand_varref("#{prefix}#{varval}#{suffix}")
elsif varval.is_a?(Array) elsif varval.is_a?(Array)
varval.map {|vv| expand_varref("#{prefix}#{vv}#{suffix}")}.flatten varval.map {|vv| expand_varref("#{prefix}#{vv}#{suffix}")}.flatten
else else
raise "I do not know how to expand a variable reference to a #{varval.class.name} (from #{varname.inspect} => #{@vars[varname].inspect})" raise "I do not know how to expand a variable reference to a #{varval.class.name} (from #{varname.inspect} => #{self[varname].inspect})"
end end
else else
varref varref
end end
end end
end end
private
# Move all VarSet variables into the copy-on-access list.
def coa!
unless @my_vars.empty?
@coa_vars.unshift(@my_vars)
@my_vars = {}
end
end
# Create a deep copy of an object.
# @param obj [nil, String, Array, Hash] Object to deep copy.
# @return [nil, String, Array, Hash] Deep copied value.
def deep_dup(obj)
obj_class = obj.class
if obj_class == Hash
obj.reduce({}) do |result, (k, v)|
result[k] = deep_dup(v)
result
end
elsif obj_class == Array
obj.map { |v| deep_dup(v) }
elsif obj_class == String
obj.dup
else
obj
end
end
end end
end end
+1 -1
View File
@@ -1,4 +1,4 @@
module Rscons module Rscons
# gem version # gem version
VERSION = "1.2.0" VERSION = "1.4.0"
end end
+56
View File
@@ -1,5 +1,14 @@
require 'fileutils' require 'fileutils'
class Dir
class << self
alias_method :orig_bracket, :[]
end
def self.[](*args)
orig_bracket(*args).sort
end
end
describe Rscons do describe Rscons do
BUILD_TEST_RUN_DIR = "build_test_run" BUILD_TEST_RUN_DIR = "build_test_run"
@@ -180,6 +189,23 @@ describe Rscons do
lines.should == ["CC build_root/src/one/one.o", "CC build_root/src/two/two.o", "LD build_dir"] lines.should == ["CC build_root/src/one/one.o", "CC build_root/src/two/two.o", "LD build_dir"]
end end
it "expands target and source paths starting with ^/ to be relative to the build root" do
test_dir('build_dir')
Rscons::Environment.new(echo: :command) do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_root = "build_root"
FileUtils.mkdir_p(env.build_root)
FileUtils.mv("src/one/one.c", "build_root")
env.Object("^/one.o", "^/one.c")
env.Program('build_dir', Dir['src/**/*.c'] + ["^/one.o"])
end
lines.should == [
%q{gcc -c -o build_root/one.o -MMD -MF build_root/one.mf -Isrc/one/ -Isrc/two/ build_root/one.c},
%q{gcc -c -o build_root/src/two/two.o -MMD -MF build_root/src/two/two.mf -Isrc/one/ -Isrc/two/ src/two/two.c},
%q{gcc -o build_dir build_root/src/two/two.o build_root/one.o},
]
end
it 'cleans built files' do it 'cleans built files' do
test_dir('build_dir') test_dir('build_dir')
Rscons::Environment.new do |env| Rscons::Environment.new do |env|
@@ -428,4 +454,34 @@ EOF
`./hello-d`.rstrip.should == "Hello from D!" `./hello-d`.rstrip.should == "Hello from D!"
end end
end end
it "supports disassembling object files" do
test_dir("simple")
Rscons::Environment.new do |env|
env.Object("simple.o", "simple.c")
env.Disassemble("simple.txt", "simple.o")
end
File.exists?("simple.txt").should be_true
File.read("simple.txt").should =~ /Disassembly of section .text:/
end
it "supports preprocessing C sources" do
test_dir("simple")
Rscons::Environment.new do |env|
env.Preprocess("simplepp.c", "simple.c")
env.Program("simple", "simplepp.c")
end
File.read("simplepp.c").should =~ /# \d+ "simple.c"/
`./simple`.should == "This is a simple C program\n"
end
it "supports preprocessing C++ sources" do
test_dir("simple_cc")
Rscons::Environment.new do |env|
env.Preprocess("simplepp.cc", "simple.cc")
env.Program("simple", "simplepp.cc")
end
File.read("simplepp.cc").should =~ /# \d+ "simple.cc"/
`./simple`.should == "This is a simple C++ program\n"
end
end end
+23
View File
@@ -0,0 +1,23 @@
module Rscons
module Builders
describe CFile do
let(:env) {Environment.new}
subject {CFile.new}
it "invokes bison to create a .c file from a .y file" do
subject.should_receive(:standard_build).with("YACC parser.c", "parser.c", ["bison", "-d", "-o", "parser.c", "parser.y"], ["parser.y"], env, :cache)
subject.run("parser.c", ["parser.y"], :cache, env, {})
end
it "invokes a custom lexer to create a .cc file from a .ll file" do
env["LEX"] = "custom_lex"
subject.should_receive(:standard_build).with("LEX lexer.cc", "lexer.cc", ["custom_lex", "-o", "lexer.cc", "parser.ll"], ["parser.ll"], env, :cache)
subject.run("lexer.cc", ["parser.ll"], :cache, env, {})
end
it "raises an error when an unknown source file is specified" do
expect {subject.run("file.c", ["foo.bar"], :cache, env, {})}.to raise_error /Unknown source file .foo.bar. for CFile builder/
end
end
end
end
+85 -68
View File
@@ -5,35 +5,42 @@ module Rscons
end end
def build_from(cache) def build_from(cache)
YAML.should_receive(:load).and_return(cache) JSON.stub(:load) do
Cache.new cache
end end
Cache.instance.tap do |cache|
describe ".clear" do cache.send(:initialize!)
it "removes the cache file" do
FileUtils.should_receive(:rm_f).with(Cache::CACHE_FILE)
Cache.clear
end end
end end
describe "#initialize" do describe "#initialize" do
context "when corrupt" do context "when corrupt" do
it "prints a warning and defaults to an empty hash" do it "prints a warning and defaults to an empty hash" do
YAML.should_receive(:load).and_return("string") JSON.should_receive(:load).and_return("string")
$stderr.should_receive(:puts).with(/Warning:.*was.corrupt/) $stderr.should_receive(:puts).with(/Warning:.*was.corrupt/)
Cache.new.instance_variable_get(:@cache).is_a?(Hash).should be_true c = Cache.instance
c.send(:initialize!)
c.instance_variable_get(:@cache).is_a?(Hash).should be_true
end end
end end
end end
describe "#clear" do
it "removes the cache file" do
FileUtils.should_receive(:rm_f).with(Cache::CACHE_FILE)
JSON.stub(:load) {{}}
Cache.instance.clear
end
end
describe "#write" do describe "#write" do
it "should fill in :version and write to file" do it "should fill in 'version' and write to file" do
cache = {} cache = {}
fh = $stdout fh = $stdout
fh.should_receive(:puts) fh.should_receive(:puts)
File.should_receive(:open).and_yield(fh) File.should_receive(:open).and_yield(fh)
build_from(cache).write build_from(cache).write
cache[:version].should == Rscons::VERSION cache["version"].should == Rscons::VERSION
end end
end end
@@ -54,7 +61,7 @@ module Rscons
end end
it "returns false when the target's checksum does not match" do it "returns false when the target's checksum does not match" do
_cache = {targets: {"target" => {checksum: "abc"}}} _cache = {"targets" => {"target" => {"checksum" => "abc"}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("def") cache.should_receive(:calculate_checksum).with("target").and_return("def")
@@ -62,7 +69,7 @@ module Rscons
end end
it "returns false when the build command has changed" do it "returns false when the build command has changed" do
_cache = {targets: {"target" => {checksum: "abc", command: "old command"}}} _cache = {"targets" => {"target" => {"checksum" => "abc", "command" => Digest::MD5.hexdigest("old command".inspect)}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc") cache.should_receive(:calculate_checksum).with("target").and_return("abc")
@@ -70,9 +77,9 @@ module Rscons
end end
it "returns false when there is a new dependency" do it "returns false when there is a new dependency" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1"}]}}} "deps" => [{"fname" => "dep.1"}]}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc") cache.should_receive(:calculate_checksum).with("target").and_return("abc")
@@ -80,15 +87,15 @@ module Rscons
end end
it "returns false when a dependency's checksum has changed" do it "returns false when a dependency's checksum has changed" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1", "deps" => [{"fname" => "dep.1",
checksum: "dep.1.chk"}, "checksum" => "dep.1.chk"},
{fname: "dep.2", {"fname" => "dep.2",
checksum: "dep.2.chk"}, "checksum" => "dep.2.chk"},
{fname: "extra.dep", {"fname" => "extra.dep",
checksum: "extra.dep.chk"}], "checksum" => "extra.dep.chk"}],
user_deps: []}}} "user_deps" => []}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc") cache.should_receive(:calculate_checksum).with("target").and_return("abc")
@@ -98,15 +105,15 @@ module Rscons
end end
it "returns false with strict_deps=true when cache has an extra dependency" do it "returns false with strict_deps=true when cache has an extra dependency" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1", "deps" => [{"fname" => "dep.1",
checksum: "dep.1.chk"}, "checksum" => "dep.1.chk"},
{fname: "dep.2", {"fname" => "dep.2",
checksum: "dep.2.chk"}, "checksum" => "dep.2.chk"},
{fname: "extra.dep", {"fname" => "extra.dep",
checksum: "extra.dep.chk"}], "checksum" => "extra.dep.chk"}],
user_deps: []}}} "user_deps" => []}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc") cache.should_receive(:calculate_checksum).with("target").and_return("abc")
@@ -114,10 +121,10 @@ module Rscons
end end
it "returns false when there is a new user dependency" do it "returns false when there is a new user dependency" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1"}], "deps" => [{"fname" => "dep.1"}],
user_deps: []}}} "user_deps" => []}}}
cache = build_from(_cache) cache = build_from(_cache)
env = "env" env = "env"
env.should_receive(:get_user_deps).with("target").and_return(["file.ld"]) env.should_receive(:get_user_deps).with("target").and_return(["file.ld"])
@@ -127,16 +134,16 @@ module Rscons
end end
it "returns false when a user dependency checksum has changed" do it "returns false when a user dependency checksum has changed" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1", "deps" => [{"fname" => "dep.1",
checksum: "dep.1.chk"}, "checksum" => "dep.1.chk"},
{fname: "dep.2", {"fname" => "dep.2",
checksum: "dep.2.chk"}, "checksum" => "dep.2.chk"},
{fname: "extra.dep", {"fname" => "extra.dep",
checksum: "extra.dep.chk"}], "checksum" => "extra.dep.chk"}],
user_deps: [{fname: "user.dep", "user_deps" => [{"fname" => "user.dep",
checksum: "user.dep.chk"}]}}} "checksum" => "user.dep.chk"}]}}}
cache = build_from(_cache) cache = build_from(_cache)
env = "env" env = "env"
env.should_receive(:get_user_deps).with("target").and_return(["user.dep"]) env.should_receive(:get_user_deps).with("target").and_return(["user.dep"])
@@ -150,15 +157,15 @@ module Rscons
end end
it "returns true when no condition for false is met" do it "returns true when no condition for false is met" do
_cache = {targets: {"target" => {checksum: "abc", _cache = {"targets" => {"target" => {"checksum" => "abc",
command: "command", "command" => Digest::MD5.hexdigest("command".inspect),
deps: [{fname: "dep.1", "deps" => [{"fname" => "dep.1",
checksum: "dep.1.chk"}, "checksum" => "dep.1.chk"},
{fname: "dep.2", {"fname" => "dep.2",
checksum: "dep.2.chk"}, "checksum" => "dep.2.chk"},
{fname: "extra.dep", {"fname" => "extra.dep",
checksum: "extra.dep.chk"}], "checksum" => "extra.dep.chk"}],
user_deps: []}}} "user_deps" => []}}}
cache = build_from(_cache) cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true) File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc") cache.should_receive(:calculate_checksum).with("target").and_return("abc")
@@ -180,23 +187,23 @@ module Rscons
cache.should_receive(:calculate_checksum).with("dep 2").and_return("dep 2 checksum") cache.should_receive(:calculate_checksum).with("dep 2").and_return("dep 2 checksum")
cache.should_receive(:calculate_checksum).with("user.dep").and_return("user.dep checksum") cache.should_receive(:calculate_checksum).with("user.dep").and_return("user.dep checksum")
cache.register_build("the target", "the command", ["dep 1", "dep 2"], env) cache.register_build("the target", "the command", ["dep 1", "dep 2"], env)
cached_target = cache.instance_variable_get(:@cache)[:targets]["the target"] cached_target = cache.instance_variable_get(:@cache)["targets"]["the target"]
cached_target.should_not be_nil cached_target.should_not be_nil
cached_target[:command].should == "the command" cached_target["command"].should == Digest::MD5.hexdigest("the command".inspect)
cached_target[:checksum].should == "the checksum" cached_target["checksum"].should == "the checksum"
cached_target[:deps].should == [ cached_target["deps"].should == [
{fname: "dep 1", checksum: "dep 1 checksum"}, {"fname" => "dep 1", "checksum" => "dep 1 checksum"},
{fname: "dep 2", checksum: "dep 2 checksum"}, {"fname" => "dep 2", "checksum" => "dep 2 checksum"},
] ]
cached_target[:user_deps].should == [ cached_target["user_deps"].should == [
{fname: "user.dep", checksum: "user.dep checksum"}, {"fname" => "user.dep", "checksum" => "user.dep checksum"},
] ]
end end
end end
describe "#targets" do describe "#targets" do
it "returns a list of targets that are cached" do it "returns a list of targets that are cached" do
cache = {targets: {"t1" => {}, "t2" => {}, "t3" => {}}} cache = {"targets" => {"t1" => {}, "t2" => {}, "t3" => {}}}
build_from(cache).targets.should == ["t1", "t2", "t3"] build_from(cache).targets.should == ["t1", "t2", "t3"]
end end
end end
@@ -218,11 +225,21 @@ module Rscons
cache.mkdir_p("one\\two\\four") cache.mkdir_p("one\\two\\four")
cache.directories.should == ["one/two", "one/two/three", "one/two/four"] cache.directories.should == ["one/two", "one/two/three", "one/two/four"]
end end
it "handles absolute paths" do
_cache = {}
cache = build_from(_cache)
File.should_receive(:exists?).with("/one").and_return(true)
File.should_receive(:exists?).with("/one/two").and_return(false)
FileUtils.should_receive(:mkdir).with("/one/two")
cache.mkdir_p("/one/two")
cache.directories.should == ["/one/two"]
end
end end
describe "#directories" do describe "#directories" do
it "returns a list of directories that are cached" do it "returns a list of directories that are cached" do
_cache = {directories: {"dir1" => true, "dir2" => true}} _cache = {"directories" => {"dir1" => true, "dir2" => true}}
build_from(_cache).directories.should == ["dir1", "dir2"] build_from(_cache).directories.should == ["dir1", "dir2"]
end end
end end
+27 -10
View File
@@ -140,7 +140,8 @@ module Rscons
env.Program("a.out", "main.c") env.Program("a.out", "main.c")
cache = "cache" cache = "cache"
Cache.should_receive(:new).and_return(cache) Cache.should_receive(:instance).and_return(cache)
cache.should_receive(:clear_checksum_cache!)
env.should_receive(:run_builder).with(anything, "a.out", ["main.c"], cache, {}).and_return(true) env.should_receive(:run_builder).with(anything, "a.out", ["main.c"], cache, {}).and_return(true)
cache.should_receive(:write) cache.should_receive(:write)
@@ -153,7 +154,8 @@ module Rscons
env.Object("main.o", "other.cc") env.Object("main.o", "other.cc")
cache = "cache" cache = "cache"
Cache.should_receive(:new).and_return(cache) Cache.should_receive(:instance).and_return(cache)
cache.should_receive(:clear_checksum_cache!)
env.should_receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return("main.o") env.should_receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return("main.o")
env.should_receive(:run_builder).with(anything, "a.out", ["main.o"], cache, {}).and_return("a.out") env.should_receive(:run_builder).with(anything, "a.out", ["main.o"], cache, {}).and_return("a.out")
cache.should_receive(:write) cache.should_receive(:write)
@@ -167,12 +169,28 @@ module Rscons
env.Object("main.o", "other.cc") env.Object("main.o", "other.cc")
cache = "cache" cache = "cache"
Cache.should_receive(:new).and_return(cache) Cache.should_receive(:instance).and_return(cache)
cache.should_receive(:clear_checksum_cache!)
env.should_receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return(false) env.should_receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return(false)
cache.should_receive(:write) cache.should_receive(:write)
expect { env.process }.to raise_error BuildError, /Failed.to.build.main.o/ expect { env.process }.to raise_error BuildError, /Failed.to.build.main.o/
end end
it "writes the cache when the Builder raises an exception" do
env = Environment.new
env.Object("module.o", "module.c")
cache = "cache"
Cache.should_receive(:instance).and_return(cache)
cache.should_receive(:clear_checksum_cache!)
env.stub(:run_builder) do |builder, target, sources, cache, vars|
raise "Ruby exception thrown by builder"
end
cache.should_receive(:write)
expect { env.process }.to raise_error RuntimeError, /Ruby exception thrown by builder/
end
end end
describe "#clear_targets" do describe "#clear_targets" do
@@ -219,7 +237,7 @@ module Rscons
it "prints the short description and executes the command" do it "prints the short description and executes the command" do
env = Environment.new(echo: :short) env = Environment.new(echo: :short)
env.should_receive(:puts).with("short desc") env.should_receive(:puts).with("short desc")
env.should_receive(:system).with("a", "command", {}).and_return(true) env.should_receive(:system).with("a", "command").and_return(true)
env.execute("short desc", ["a", "command"]) env.execute("short desc", ["a", "command"])
end end
end end
@@ -228,7 +246,7 @@ module Rscons
it "prints the short description, executes the command, and prints the failed command line" do it "prints the short description, executes the command, and prints the failed command line" do
env = Environment.new(echo: :short) env = Environment.new(echo: :short)
env.should_receive(:puts).with("short desc") env.should_receive(:puts).with("short desc")
env.should_receive(:system).with("a", "command", {}).and_return(false) env.should_receive(:system).with("a", "command").and_return(false)
$stdout.should_receive(:write).with("Failed command was: ") $stdout.should_receive(:write).with("Failed command was: ")
env.should_receive(:puts).with("a command") env.should_receive(:puts).with("a command")
env.execute("short desc", ["a", "command"]) env.execute("short desc", ["a", "command"])
@@ -240,8 +258,8 @@ module Rscons
it "prints the command executed and executes the command" do it "prints the command executed and executes the command" do
env = Environment.new(echo: :command) env = Environment.new(echo: :command)
env.should_receive(:puts).with("a command '--arg=val with spaces'") env.should_receive(:puts).with("a command '--arg=val with spaces'")
env.should_receive(:system).with("a", "command", "--arg=val with spaces", opt: :val).and_return(false) env.should_receive(:system).with({modified: :environment}, "a", "command", "--arg=val with spaces", {opt: :val}).and_return(false)
env.execute("short desc", ["a", "command", "--arg=val with spaces"], opt: :val) env.execute("short desc", ["a", "command", "--arg=val with spaces"], env: {modified: :environment}, options: {opt: :val})
end end
end end
end end
@@ -249,8 +267,7 @@ module Rscons
describe "#method_missing" do describe "#method_missing" do
it "calls the original method missing when the target method is not a known builder" do it "calls the original method missing when the target method is not a known builder" do
env = Environment.new env = Environment.new
env.should_receive(:orig_method_missing).with(:foobar) expect {env.foobar}.to raise_error /undefined method .foobar./
env.foobar
end end
it "records the target when the target method is a known builder" do it "records the target when the target method is a known builder" do
@@ -260,7 +277,7 @@ module Rscons
target = env.instance_variable_get(:@targets)["target"] target = env.instance_variable_get(:@targets)["target"]
target.should_not be_nil target.should_not be_nil
target[:builder].is_a?(Builder).should be_true target[:builder].is_a?(Builder).should be_true
target[:source].should == ["src1", "src2"] target[:sources].should == ["src1", "src2"]
target[:vars].should == {var: "val"} target[:vars].should == {var: "val"}
target[:args].should == [] target[:args].should == []
end end
+63 -17
View File
@@ -1,6 +1,6 @@
module Rscons module Rscons
describe VarSet do describe VarSet do
describe '.initialize' do describe '#initialize' do
it "initializes variables from a Hash" do it "initializes variables from a Hash" do
v = VarSet.new({"one" => 1, "two" => :two}) v = VarSet.new({"one" => 1, "two" => :two})
v["one"].should == 1 v["one"].should == 1
@@ -20,10 +20,10 @@ module Rscons
end end
end end
describe :[] do describe "#[]" do
v = VarSet.new({"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [3, 6], "qux" => {a: :b}})
it "allows accessing a variable with its verbatim value if type is not specified" do it "allows accessing a variable with its verbatim value if type is not specified" do
v = VarSet.new({"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [3, 6], "qux" => {a: :b}})
v["fuz"].should == "a string" v["fuz"].should == "a string"
v["foo"].should == 42 v["foo"].should == 42
v["bar"].should == :baz v["bar"].should == :baz
@@ -32,7 +32,7 @@ module Rscons
end end
end end
describe :[]= do describe "#[]=" do
it "allows assigning to variables" do it "allows assigning to variables" do
v = VarSet.new("CFLAGS" => ["-Wall", "-O3"]) v = VarSet.new("CFLAGS" => ["-Wall", "-O3"])
v["CPPPATH"] = ["one", "two"] v["CPPPATH"] = ["one", "two"]
@@ -41,40 +41,86 @@ module Rscons
end end
end end
describe '.append' do describe "#include?" do
it "returns whether the variable is in the VarSet" do
v = VarSet.new("CFLAGS" => [], :foo => :bar)
expect(v.include?("CFLAGS")).to be_true
expect(v.include?(:CFLAGS)).to be_false
expect(v.include?(:foo)).to be_true
expect(v.include?("foo")).to be_false
expect(v.include?("bar")).to be_false
v2 = v.clone
v2.append("bar" => [])
expect(v2.include?("CFLAGS")).to be_true
expect(v2.include?(:CFLAGS)).to be_false
expect(v2.include?(:foo)).to be_true
expect(v2.include?("foo")).to be_false
expect(v2.include?("bar")).to be_true
end
end
describe '#append' do
it "adds values from a Hash to the VarSet" do it "adds values from a Hash to the VarSet" do
v = VarSet.new("LDFLAGS" => "-lgcc") v = VarSet.new("LDFLAGS" => "-lgcc")
v.append("LIBS" => "gcc", "LIBPATH" => ["mylibs"]) v.append("LIBS" => "gcc", "LIBPATH" => ["mylibs"])
v.vars.keys.should =~ ["LDFLAGS", "LIBS", "LIBPATH"] expect(v["LDFLAGS"]).to eq("-lgcc")
expect(v["LIBS"]).to eq("gcc")
expect(v["LIBPATH"]).to eq(["mylibs"])
end end
it "adds values from another VarSet to the VarSet" do it "adds values from another VarSet to the VarSet" do
v = VarSet.new("CPPPATH" => ["mydir"]) v = VarSet.new("CPPPATH" => ["mydir"])
v2 = VarSet.new("CFLAGS" => ["-O0"], "CPPPATH" => ["different_dir"]) v2 = VarSet.new("CFLAGS" => ["-O0"], "CPPPATH" => ["different_dir"])
v.append(v2) v.append(v2)
v.vars.keys.should =~ ["CPPPATH", "CFLAGS"] expect(v["CFLAGS"]).to eq(["-O0"])
v["CPPPATH"].should == ["different_dir"] expect(v["CPPPATH"]).to eq(["different_dir"])
end
it "does not pick up subsequent variable changes from a given VarSet" do
v = VarSet.new("dirs" => ["a"])
v2 = VarSet.new
v2.append(v)
v["dirs"] << "b"
expect(v["dirs"]).to eq(["a", "b"])
expect(v2["dirs"]).to eq(["a"])
end end
end end
describe '.merge' do describe '#merge' do
it "returns a new VarSet merged with the given Hash" do it "returns a new VarSet merged with the given Hash" do
v = VarSet.new("foo" => "yoda") v = VarSet.new("foo" => "yoda")
v2 = v.merge("baz" => "qux") v2 = v.merge("baz" => "qux")
v.vars.keys.should == ["foo"] expect(v["foo"]).to eq("yoda")
v2.vars.keys.should =~ ["foo", "baz"] expect(v2["foo"]).to eq("yoda")
expect(v2["baz"]).to eq("qux")
end end
it "returns a new VarSet merged with the given VarSet" do it "returns a new VarSet merged with the given VarSet" do
v = VarSet.new("foo" => ["a", "b"], "bar" => 42) v = VarSet.new("foo" => ["a", "b"], "bar" => 42)
v2 = v.merge(VarSet.new("bar" => 33, "baz" => :baz)) v2 = v.merge(VarSet.new("bar" => 33, "baz" => :baz))
v2["foo"] << "c" v2["foo"] << "c"
v["foo"].should == ["a", "b"] expect(v["foo"]).to eq ["a", "b"]
v["bar"].should == 42 expect(v["bar"]).to eq 42
v2["foo"].should == ["a", "b", "c"] expect(v2["foo"]).to eq ["a", "b", "c"]
v2["bar"].should == 33 expect(v2["bar"]).to eq 33
end
it "does not pick up subsequent variable changes from a given VarSet" do
v = VarSet.new("var" => ["a", "b"], "var2" => ["1", "2"])
v["var2"] << "3"
v2 = v.clone
v["var"] << "c"
expect(v["var"]).to eq(["a", "b", "c"])
expect(v["var2"]).to eq(["1", "2", "3"])
expect(v2["var"]).to eq(["a", "b"])
expect(v2["var2"]).to eq(["1", "2", "3"])
end end
end end
describe '.expand_varref' do describe '#expand_varref' do
v = VarSet.new("CFLAGS" => ["-Wall", "-O2"], v = VarSet.new("CFLAGS" => ["-Wall", "-O2"],
"CC" => "gcc", "CC" => "gcc",
"CPPPATH" => ["dir1", "dir2"], "CPPPATH" => ["dir1", "dir2"],
+2 -2
View File
@@ -2,7 +2,7 @@ describe Rscons do
describe ".clean" do describe ".clean" do
it "removes all build targets and created directories" do it "removes all build targets and created directories" do
cache = "cache" cache = "cache"
Rscons::Cache.should_receive(:new).and_return(cache) Rscons::Cache.should_receive(:instance).and_return(cache)
cache.should_receive(:targets).and_return(["build/a.out", "build/main.o"]) cache.should_receive(:targets).and_return(["build/a.out", "build/main.o"])
FileUtils.should_receive(:rm_f).with("build/a.out") FileUtils.should_receive(:rm_f).with("build/a.out")
FileUtils.should_receive(:rm_f).with("build/main.o") FileUtils.should_receive(:rm_f).with("build/main.o")
@@ -18,7 +18,7 @@ describe Rscons do
Dir.should_receive(:rmdir).with("build") Dir.should_receive(:rmdir).with("build")
File.should_receive(:directory?).with("other").and_return(true) File.should_receive(:directory?).with("other").and_return(true)
Dir.should_receive(:entries).with("other").and_return([".", "..", "other.file"]) Dir.should_receive(:entries).with("other").and_return([".", "..", "other.file"])
Rscons::Cache.should_receive(:clear) cache.should_receive(:clear)
Rscons.clean Rscons.clean
end end