Compare commits

...
68 Commits
Author SHA1 Message Date
josh af0679708c v1.1.0 2014-02-17 16:37:59 -05:00
josh 4b0d0c3957 README: add Release Notes section 2014-02-17 16:15:49 -05:00
josh 202f10b86f README: add example for custom builder with multiple output files 2014-02-17 16:14:10 -05:00
josh 1d47db3144 support passing multiple targets to Cache#up_to_date? and #register_build 2014-02-17 16:10:15 -05:00
josh f7b7a4fefa v1.0.0 2014-02-14 10:46:31 -05:00
josh 00b038ef50 use require_relative instead of require 2014-02-14 10:37:41 -05:00
josh 98f8f38715 remove String#set_suffix monkey patch 2014-02-14 10:36:15 -05:00
josh 2ffb5d525a remove String#absolute_path? monkey patch 2014-02-14 10:32:57 -05:00
josh fa7d17675b remove String#has_suffix? monkey-patch; use String#end_with? 2014-02-14 10:20:32 -05:00
josh b43c473762 README: add link to API documentation at rubydoc.info 2014-02-14 09:57:46 -05:00
josh f278b211f7 README: add more information about custom builders 2014-02-14 09:50:39 -05:00
josh c897bcf06e README.md fixes/tweaks 2014-02-14 09:42:04 -05:00
josh 7c89dddbd6 v0.3.1 2014-01-23 14:40:45 -05:00
josh c07d9f86a6 bugfix: loading a Cache without deps or user_deps stored causes an exception 2014-01-23 14:40:30 -05:00
josh ff26da8865 fix custom builder examples in README.md 2014-01-15 16:57:46 -05:00
josh 5ff555cf6a add Environment#expand_varref() 2014-01-02 11:49:08 -05:00
josh e3aadc562f Rscons::Varset: remove unused type parameter to #[] 2014-01-02 11:05:15 -05:00
josh 7ff2ec1f1a v0.3.0 2013-12-31 14:10:47 -05:00
josh 5362f761e6 move default builder classes into Rscons::Builders namespace module 2013-12-31 13:31:17 -05:00
josh cdb3352b4e remove module monkey-patching; add Builder#name() 2013-12-31 13:10:00 -05:00
josh 08bc390d40 Rscons::BuildError inherits from RuntimeError instead of Exception 2013-12-31 13:08:38 -05:00
josh f8af0630b0 pass env to Cache#up_to_date?() and #register_build() instead of user_deps 2013-12-31 12:46:30 -05:00
josh 1868193b54 do not prepend build_root to source files that are already inside build_root 2013-12-31 10:15:19 -05:00
josh 8976d7dea2 update README custom builder examples 2013-12-27 15:01:02 -05:00
josh 295324eafd add Environment#depends() to specify user dependencies for a build target 2013-12-27 14:31:46 -05:00
josh 7330e74ab8 modify Cache to store user-specified extra dependencies 2013-12-27 14:20:32 -05:00
josh a28cef9eb7 change "vars" parameters to be required 2013-12-27 13:53:40 -05:00
josh e667046da8 add Environment#clear_targets() 2013-12-27 13:35:16 -05:00
josh 539a49fe90 do not allow construction variables as arguments to Environment#initialize() or Environment#clone() 2013-12-27 12:10:54 -05:00
josh 7681c7584f add String#absolute_path? 2013-12-27 11:40:24 -05:00
josh 8fcbd39057 v0.2.2 2013-11-13 23:52:33 -05:00
josh d0bbc78751 Program: do not continue executing builder if building a source fails 2013-11-13 23:52:15 -05:00
josh 7249e57951 add info to error message when expanding unknown variable types 2013-11-13 23:50:46 -05:00
josh d01913641f v0.2.1 2013-11-11 11:36:10 -05:00
josh d5be569cd5 fix Environment#clone() to allow subclassing 2013-11-11 11:35:59 -05:00
josh 4d7ed373e7 v0.2.0 2013-11-08 15:45:35 -05:00
josh 72558391a2 clarify documentation about cloning Environments 2013-11-08 15:45:24 -05:00
josh d01767981c add Environment#build_root to be used if no build directories match 2013-11-08 14:52:39 -05:00
josh 01c847fa32 v0.1.0 2013-11-06 13:38:59 -05:00
josh 20f509ec27 rename *COM construction variables to *CMD 2013-11-06 13:35:41 -05:00
josh 243eb9a4ab rename "tweakers" to "build hooks" 2013-11-06 13:32:56 -05:00
josh 49be46199f define our own clean/clobber tasks 2013-11-06 13:29:24 -05:00
josh c9b670ae5c change default "echo" setting to :short 2013-11-06 13:22:30 -05:00
josh 6bcb984e8e README: add gem version badge 2013-11-06 13:18:09 -05:00
josh b377940252 begin expanding README documentation 2013-11-06 12:45:50 -05:00
josh 2e0c2688aa VarSet#expand_varref(): fix recursive expansion 2013-11-05 17:06:09 -05:00
josh 66f53a23f6 change construction variable reference format to ${var}
this change is backwards-incompatible
2013-11-05 16:54:01 -05:00
josh 545b893ef4 clean up a couple YARD warnings 2013-11-05 16:37:18 -05:00
josh 30564af558 add build test for D 2013-11-05 16:29:15 -05:00
josh cc2c2733fc move build_tests_spec.rb back into spec, remove build_tests rake task 2013-11-05 16:22:09 -05:00
josh ddb5357b02 explicitly set LD construction variable in one build test 2013-11-05 16:19:02 -05:00
josh b805e5379f execute all build tests in the top-level rake context instead of spawning subprocesses 2013-11-05 16:11:08 -05:00
josh 71344b4782 Program: do not check env.build_sources() return value (it will raise an exception if something was wrong) 2013-11-05 15:41:47 -05:00
josh b1cdd3d1db Object: simplify handling of known suffixes 2013-11-05 15:25:10 -05:00
josh ed11b05b97 add rscons_spec.rb 2013-11-05 14:37:26 -05:00
josh a418d31000 finish up Environment specs 2013-11-05 14:28:43 -05:00
josh 797580e937 Environment: remove support for specifying :builders key in constructor 2013-11-05 14:18:02 -05:00
josh 8b852bd2fd Environment: more specs 2013-11-05 14:14:03 -05:00
josh e863c9a564 Environment: rework process()
- improve efficiency
- give errors for the dependency that failed instead of the top-level target
2013-11-05 12:43:50 -05:00
josh 7ce5a8f9d9 working on Environment specs 2013-11-05 12:26:42 -05:00
josh 1280cfb465 change Environment#parse_makefile_deps() to a class method 2013-11-05 11:44:07 -05:00
josh 0cd9dbd1bd add separate spec_helper.rb for build_tests directory 2013-11-05 11:36:30 -05:00
josh b38c56d446 move build_tests_spec.rb to build_tests directory; add build_tests task 2013-11-05 11:05:45 -05:00
josh d2b7c93f53 rename Rakefile -> Rakefile.rb 2013-11-05 10:52:56 -05:00
josh 67922b887d remove Environment#targets() and Environment#target_sources() which didn't seem to be used 2013-11-05 10:42:01 -05:00
josh 8100830c16 spec Cache 2013-11-05 10:25:36 -05:00
josh cb629bf7ec do not include spec files in simplecov report 2013-11-05 10:25:34 -05:00
josh 65a5838605 Cache: set :version when writing to file 2013-11-05 09:22:46 -05:00
31 changed files with 1359 additions and 390 deletions

No files matched your search

+1 -1
View File
@@ -15,4 +15,4 @@ spec/reports
test/tmp test/tmp
test/version_tmp test/version_tmp
tmp tmp
build_tests_run build_test_run
+227 -12
View File
@@ -2,11 +2,13 @@
Software construction library inspired by SCons and implemented in Ruby Software construction library inspired by SCons and implemented in Ruby
[![Gem Version](https://badge.fury.io/rb/rscons.png)](http://badge.fury.io/rb/rscons)
## Installation ## Installation
Add this line to your application's Gemfile: Add this line to your application's Gemfile:
gem 'rscons' gem "rscons"
And then execute: And then execute:
@@ -18,20 +20,233 @@ Or install it yourself as:
## Usage ## Usage
RScons::Environment.new do |env| Rscons is a Ruby library.
env.Program('program', Dir['*.c']) It can be called from a standalone Ruby script or it can be used with rake and
end called from your Rakefile.
main_env = RScons::Environment.new do |env| ### Example: Building a C Program
env.build_dir('src', 'build/main')
env.Program('program', Dir['src/**/*.cc'])
end
debug_env = main_env.clone do |env| ```ruby
env.build_dir('src', 'build/debug') Rscons::Environment.new do |env|
env['CFLAGS'] = ['-g', '-O0'] env["CFLAGS"] << "-Wall"
env.Program('program-debug', Dir['src/**/*.cc']) env.Program("program", Dir["**/*.c"])
end
```
### Example: Building a D Program
```ruby
Rscons::Environment.new do |env|
env["DFLAGS"] << "-Wall"
env.Program("program", Dir["**/*.d"])
end
```
### Example: Cloning an Environment
```ruby
main_env = Rscons::Environment.new do |env|
# Store object files from sources under "src" in "build/main"
env.build_dir("src", "build/main")
env["CFLAGS"] = ["-DSOME_DEFINE", "-O3"]
env["LIBS"] = ["SDL"]
env.Program("program", Dir["src/**/*.cc"])
end
debug_env = main_env.clone do |env|
# Store object files from sources under "src" in "build/debug"
env.build_dir("src", "build/debug")
env["CFLAGS"] -= ["-O3"]
env["CFLAGS"] += ["-g", "-O0"]
env.Program("program-debug", Dir["src/**/*.cc"])
end
```
### Example: Custom Builder
Custom builders are implemented as classes which extend from `Rscons::Builder`.
The builder must have a `run` method which is called to invoke the builder.
The `run` method should return the name of the target built on success, and
`false` on failure.
```ruby
class GenerateFoo < Rscons::Builder
def run(target, sources, cache, env, vars)
cache.mkdir_p(File.dirname(target))
File.open(target, "w") do |fh|
fh.puts <<EOF
#define GENERATED 42
EOF
end end
target
end
end
Rscons::Environment.new do |env|
env.add_builder(GenerateFoo.new)
env.GenerateFoo("foo.h", [])
env.Program("a.out", Dir["*.c"])
end
```
### Example: Custom Builder That Only Regenerates When Necessary
```ruby
class CmdBuilder < Rscons::Builder
def run(target, sources, cache, env, vars)
cmd = ["cmd", "-i", sources.first, "-o", target]
unless cache.up_to_date?(target, cmd, sources, env)
cache.mkdir_p(File.dirname(target))
system(cmd)
cache.register_build(target, cmd, sources, env)
end
target
end
end
Rscons::Environment.new do |env|
env.add_builder(CmdBuilder.new)
env.CmdBuilder("foo.gen", "foo_gen.cfg")
end
```
### Example: Custom Builder That Generates Multiple Output Files
```ruby
class CModuleGenerator < Rscons::Builder
def run(target, sources, cache, env, vars)
c_fname = target
h_fname = target.sub(/\.c$/, ".h")
cmd = ["generate_c_and_h", sources.first, c_fname, h_fname]
unless cache.up_to_date?([c_fname, h_fname], cmd, sources, env)
cache.mkdir_p(File.dirname(target))
system(cmd)
cache.register_build([c_fname, h_fname], cmd, sources, env)
end
target
end
end
Rscons::Environment.new do |env|
env.add_builder(CModuleGenerator.new)
env.CModuleGenerator("build/foo.c", "foo_gen.cfg")
end
```
### Example: Custom Builder Using Builder#standard_build()
The `standard_build` method from the `Rscons::Builder` base class can be used
when the builder needs to execute a system command to produce the target file.
The `standard_build` method will return the correct value so its return value
can be used as the return value from the `run` method.
```ruby
class CmdBuilder < Rscons::Builder
def run(target, sources, cache, env, vars)
cmd = ["cmd", "-i", sources.first, "-o", target]
standard_build("CmdBld #{target}", target, cmd, sources, env, cache)
end
end
Rscons::Environment.new do |env|
env.add_builder(CmdBuilder.new)
env.CmdBuilder("foo.gen", "foo_gen.cfg")
end
```
### Example: Using different compilation flags for some sources
```ruby
Rscons::Environment.new do |env|
env["CFLAGS"] = ["-O3", "-Wall", "-DDEFINE"]
env.add_build_hook do |build_op|
if build_op[:target] =~ %r{build/third-party}
build_op[:vars]["CFLAGS"] -= ["-Wall"]
end
end
env.build_dir("src", "build")
env.Program("program", Dir["**/*.cc"])
end
```
Each build hook block will be invoked for every build operation, so the block
should test the target or sources if its action should only apply to some
subset of build targets or source files.
The `build_op` parameter to the build hook block is a Hash describing the
build operation with the following keys:
* `:builder` - `Builder` instance in use
* `:target` - `String` name of the target file
* `:sources` - `Array` of the source files
* `:vars` - `Rscons::VarSet` containing the construction variables to use
The build hook can overwrite entries in `build_op[:vars]` to alter the
construction variables in use for this specific build operation.
### Example: Creating a static library
```ruby
Rscons::Environment.new do |env|
env.Library("mylib.a", Dir["src/**/*.c"])
end
```
## Details
### Default Builders
Rscons ships with a number of default builders:
* Library, which collects object files into a static library archive file
* Object, which compiles source files to produce an object file
* Program, which links object files to produce an executable
If you want to create an Environment that does not contain any builders,
you can use the `exclude_builders` key to the Environment constructor.
### Managing Environments
An Rscons::Environment consists of:
* a collection of construction variables
* a collection of builders
* a mapping of build directories from source directories
* a default build root to apply if no build directories are matched
* a collection of targets to build
* a collection of build hooks
When cloning an environment, the construction variables and builders 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.
Cloned environments contain "deep copies" of construction variables.
For example, in:
```ruby
base_env = Rscons::Environment.new
base_env["CPPPATH"] = ["one", "two"]
cloned_env = base_env.clone
cloned_env["CPPPATH"] << "three"
```
`base_env["CPPPATH"]` will not include "three".
### Construction Variable Naming
* uppercase strings - the default construction variables that Rscons uses
* lowercase symbols - Rscons options
* lowercase strings - reserved as user-defined construction variables
### API documentation
Documentation for the complete Rscons API can be found at
http://rubydoc.info/github/holtrop/rscons/frames.
## Release Notes
### v1.1.0
- Change Cache#up_to_date?() and #register_build() to accept a single target
file or an array of target file names
## Contributing ## Contributing
+8 -3
View File
@@ -7,11 +7,8 @@ end
require "bundler/gem_tasks" require "bundler/gem_tasks"
require "rspec/core/rake_task" require "rspec/core/rake_task"
require "rake/clean"
require "yard" require "yard"
CLEAN.include 'build_tests_run'
RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new(:spec)
YARD::Rake::YardocTask.new do |yard| YARD::Rake::YardocTask.new do |yard|
@@ -19,3 +16,11 @@ YARD::Rake::YardocTask.new do |yard|
end end
task :default => :spec task :default => :spec
task :clean do
FileUtils.rm_rf(["build_test_run", "doc", "coverage"])
end
task :clobber => :clean do
FileUtils.rm_rf(["pkg"])
end
-5
View File
@@ -1,5 +0,0 @@
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.Program('build_dir', Dir['src/**/*.c'])
end
-12
View File
@@ -1,12 +0,0 @@
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.add_tweaker do |build_op|
if build_op[:target] =~ %r{build_one/.*\.o}
build_op[:vars]["CFLAGS"] << "-O1"
elsif build_op[:target] =~ %r{build_two/.*\.o}
build_op[:vars]["CFLAGS"] << "-O2"
end
end
env.Program('tweaker', Dir['src/**/*.c'])
end
-11
View File
@@ -1,11 +0,0 @@
debug = Rscons::Environment.new do |env|
env.build_dir('src', 'debug')
env['CFLAGS'] = '-O2'
env['CPPFLAGS'] = '-DSTRING="Debug Version"'
env.Program('program-debug', Dir['src/*.c'])
end
release = debug.clone('CPPFLAGS' => '-DSTRING="Release Version"') do |env|
env.build_dir('src', 'release')
env.Program('program-release', Dir['src/*.c'])
end
-15
View File
@@ -1,15 +0,0 @@
class MySource < Rscons::Builder
def run(target, sources, cache, env, vars = {})
File.open(target, 'w') do |fh|
fh.puts <<EOF
#define THE_VALUE 5678
EOF
end
target
end
end
Rscons::Environment.new(echo: :short, builders: [MySource.new]) do |env|
env.MySource('inc.h', [])
env.Program('program', Dir['*.c'])
end
+6
View File
@@ -0,0 +1,6 @@
import std.stdio;
void main()
{
writeln("Hello from D!");
}
-3
View File
@@ -1,3 +0,0 @@
Rscons::Environment.new(echo: :short) do |env|
env.Program('header', Dir['*.c'])
end
-4
View File
@@ -1,4 +0,0 @@
Rscons::Environment.new(echo: :command) do |env|
env.Program('library', ['lib.a', 'three.c'])
env.Library("lib.a", ['one.c', 'two.c'], 'CPPFLAGS' => ['-Dmake_lib'])
end
-4
View File
@@ -1,4 +0,0 @@
Rscons::Environment.new do |env|
# CHANGE FLAGS
env.Program('simple', Dir['*.c'])
end
-4
View File
@@ -1,4 +0,0 @@
Rscons::Environment.new do |env|
# CHANGE FLAGS
env.Program('simple', Dir['*.cc'])
end
-4
View File
@@ -1,4 +0,0 @@
Rscons::Environment.new(echo: :command) do |env|
env.Object("one.o", "one.c", 'CPPFLAGS' => ['-DONE'])
env.Program('two_sources', ['one.o', 'two.c'])
end
+25 -16
View File
@@ -1,27 +1,23 @@
require "rscons/builder" require_relative "rscons/builder"
require "rscons/cache" require_relative "rscons/cache"
require "rscons/environment" require_relative "rscons/environment"
require "rscons/varset" require_relative "rscons/varset"
require "rscons/version" require_relative "rscons/version"
require "rscons/monkey/module"
require "rscons/monkey/string"
# default builders # default builders
require "rscons/builders/library" require_relative "rscons/builders/library"
require "rscons/builders/object" require_relative "rscons/builders/object"
require "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 = [
Library, :Library,
Object, :Object,
Program, :Program,
] ]
class BuildError < Exception class BuildError < RuntimeError; end
end
# Remove all generated files # Remove all generated files
def self.clean def self.clean
@@ -39,4 +35,17 @@ module Rscons
end end
Cache.clear Cache.clear
end end
# Return whether the given path is an absolute filesystem path or not
# @param path [String] the path to examine
def self.absolute_path?(path)
path =~ %r{^(/|\w:[\\/])}
end
# Return a new path by changing the suffix in path to suffix.
# @param path [String] the path to alter
# @param suffix [String] the new filename suffix
def self.set_suffix(path, suffix)
path.sub(/\.[^.]*$/, suffix)
end
end end
+11 -2
View File
@@ -1,8 +1,17 @@
require "fileutils" require "fileutils"
module Rscons module Rscons
# Namespace module in which to store builders for convenient grouping
module Builders; end
# Class to hold an object that knows how to build a certain type of file. # Class to hold an object that knows how to build a certain type of file.
class Builder class Builder
# Return the name of the builder.
# If not overridden this defaults to the last component of the class name.
def name
self.class.name.split(":").last
end
# Return a set of default variable values for the Environment to use # Return a set of default variable values for the Environment to use
# unless the user overrides any. # unless the user overrides any.
# @param env [Environment] The Environment. # @param env [Environment] The Environment.
@@ -23,11 +32,11 @@ module Rscons
# build command. # build command.
# Return the name of the target or false on failure. # Return the name of the target or false on failure.
def standard_build(short_cmd_string, target, command, sources, env, cache) def standard_build(short_cmd_string, target, command, sources, env, cache)
unless cache.up_to_date?(target, command, sources) unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target)) cache.mkdir_p(File.dirname(target))
FileUtils.rm_f(target) FileUtils.rm_f(target)
return false unless env.execute(short_cmd_string, command) return false unless env.execute(short_cmd_string, command)
cache.register_build(target, command, sources) cache.register_build(target, command, sources, env)
end end
target target
end end
+6 -6
View File
@@ -1,18 +1,17 @@
require 'fileutils'
module Rscons module Rscons
module Builders
# A default RScons builder that produces a static library archive. # A default RScons builder that produces a static library archive.
class Library < Builder class Rscons::Builders::Library < Rscons::Builder
def default_variables(env) def default_variables(env)
{ {
'AR' => 'ar', 'AR' => 'ar',
'LIBSUFFIX' => '.a', 'LIBSUFFIX' => '.a',
'ARFLAGS' => [], 'ARFLAGS' => [],
'ARCOM' => ['$AR', 'rcs', '$ARFLAGS', '$_TARGET', '$_SOURCES'] 'ARCMD' => ['${AR}', 'rcs', '${ARFLAGS}', '${_TARGET}', '${_SOURCES}']
} }
end end
def run(target, sources, cache, env, vars = {}) def run(target, sources, cache, env, vars)
# build sources to linkable objects # build sources to linkable objects
objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars) objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars)
if objects if objects
@@ -20,9 +19,10 @@ module Rscons
'_TARGET' => target, '_TARGET' => target,
'_SOURCES' => objects, '_SOURCES' => objects,
}) })
command = env.build_command(env['ARCOM'], vars) command = env.build_command(env['ARCMD'], vars)
standard_build("AR #{target}", target, command, objects, env, cache) standard_build("AR #{target}", target, command, objects, env, cache)
end end
end end
end end
end
end end
+33 -32
View File
@@ -1,18 +1,26 @@
module Rscons module Rscons
module Builders
# A default RScons builder which knows how to produce an object file from # A default RScons builder which knows how to produce an object file from
# various types of source files. # various types of source files.
class Object < Builder class Object < Builder
KNOWN_SUFFIXES = {
"AS" => "ASSUFFIX",
"CC" => "CSUFFIX",
"CXX" => "CXXSUFFIX",
"DC" => "DSUFFIX",
}
def default_variables(env) def default_variables(env)
{ {
'OBJSUFFIX' => '.o', 'OBJSUFFIX' => '.o',
'AS' => '$CC', 'AS' => '${CC}',
'ASFLAGS' => [], 'ASFLAGS' => [],
'ASSUFFIX' => '.S', 'ASSUFFIX' => '.S',
'ASPPPATH' => '$CPPPATH', 'ASPPPATH' => '${CPPPATH}',
'ASPPFLAGS' => '$CPPFLAGS', 'ASPPFLAGS' => '${CPPFLAGS}',
'ASDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'], 'ASDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'ASCOM' => ['$AS', '-c', '-o', '$_TARGET', '$ASDEPGEN', '-I$[ASPPPATH]', '$ASPPFLAGS', '$ASFLAGS', '$_SOURCES'], 'ASCMD' => ['${AS}', '-c', '-o', '${_TARGET}', '${ASDEPGEN}', '-I${ASPPPATH}', '${ASPPFLAGS}', '${ASFLAGS}', '${_SOURCES}'],
'CPPFLAGS' => [], 'CPPFLAGS' => [],
'CPPPATH' => [], 'CPPPATH' => [],
@@ -20,61 +28,54 @@ module Rscons
'CC' => 'gcc', 'CC' => 'gcc',
'CFLAGS' => [], 'CFLAGS' => [],
'CSUFFIX' => '.c', 'CSUFFIX' => '.c',
'CCDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'], 'CCDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'CCCOM' => ['$CC', '-c', '-o', '$_TARGET', '$CCDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CFLAGS', '$_SOURCES'], 'CCCMD' => ['${CC}', '-c', '-o', '${_TARGET}', '${CCDEPGEN}', '-I${CPPPATH}', '${CPPFLAGS}', '${CFLAGS}', '${_SOURCES}'],
'CXX' => 'g++', 'CXX' => 'g++',
'CXXFLAGS' => [], 'CXXFLAGS' => [],
'CXXSUFFIX' => '.cc', 'CXXSUFFIX' => '.cc',
'CXXDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'], 'CXXDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'CXXCOM' =>['$CXX', '-c', '-o', '$_TARGET', '$CXXDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CXXFLAGS', '$_SOURCES'], 'CXXCMD' =>['${CXX}', '-c', '-o', '${_TARGET}', '${CXXDEPGEN}', '-I${CPPPATH}', '${CPPFLAGS}', '${CXXFLAGS}', '${_SOURCES}'],
'DC' => 'gdc', 'DC' => 'gdc',
'DFLAGS' => [], 'DFLAGS' => [],
'DSUFFIX' => '.d', 'DSUFFIX' => '.d',
'D_IMPORT_PATH' => [], 'D_IMPORT_PATH' => [],
'DCCOM' => ['$DC', '-c', '-o', '$_TARGET', '-I$[D_IMPORT_PATH]', '$DFLAGS', '$_SOURCES'], 'DCCMD' => ['${DC}', '-c', '-o', '${_TARGET}', '-I${D_IMPORT_PATH}', '${DFLAGS}', '${_SOURCES}'],
} }
end end
def produces?(target, source, env) def produces?(target, source, env)
target.has_suffix?(env['OBJSUFFIX']) and ( target.end_with?(*env['OBJSUFFIX']) and KNOWN_SUFFIXES.find do |compiler, suffix_var|
source.has_suffix?(env['ASSUFFIX']) or source.end_with?(*env[suffix_var])
source.has_suffix?(env['CSUFFIX']) or end
source.has_suffix?(env['CXXSUFFIX']) or
source.has_suffix?(env['DSUFFIX']))
end end
def run(target, sources, cache, env, vars = {}) def run(target, sources, cache, env, vars)
vars = vars.merge({ vars = vars.merge({
'_TARGET' => target, '_TARGET' => target,
'_SOURCES' => sources, '_SOURCES' => sources,
'_DEPFILE' => target.set_suffix('.mf'), '_DEPFILE' => Rscons.set_suffix(target, '.mf'),
}) })
com_prefix = if sources.first.has_suffix?(env['ASSUFFIX']) com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var|
'AS' sources.first.end_with?(*env[suffix_var])
elsif sources.first.has_suffix?(env['CSUFFIX']) end.tap do |v|
'CC' v.nil? and raise "Error: unknown input file type: #{sources.first.inspect}"
elsif sources.first.has_suffix?(env['CXXSUFFIX']) end.first
'CXX' command = env.build_command(env["#{com_prefix}CMD"], vars)
elsif sources.first.has_suffix?(env['DSUFFIX']) unless cache.up_to_date?(target, command, sources, env)
'DC'
else
raise "Error: unknown input file type: #{sources.first.inspect}"
end
command = env.build_command(env["#{com_prefix}COM"], vars)
unless cache.up_to_date?(target, command, sources)
cache.mkdir_p(File.dirname(target)) cache.mkdir_p(File.dirname(target))
FileUtils.rm_f(target) FileUtils.rm_f(target)
return false unless env.execute("#{com_prefix} #{target}", command) return false unless env.execute("#{com_prefix} #{target}", command)
deps = sources deps = sources
if File.exists?(vars['_DEPFILE']) if File.exists?(vars['_DEPFILE'])
deps += env.parse_makefile_deps(vars['_DEPFILE'], target) deps += Environment.parse_makefile_deps(vars['_DEPFILE'], target)
FileUtils.rm_f(vars['_DEPFILE']) FileUtils.rm_f(vars['_DEPFILE'])
end end
cache.register_build(target, command, deps.uniq) cache.register_build(target, command, deps.uniq, env)
end end
target target
end end
end end
end
end end
+8 -7
View File
@@ -1,7 +1,8 @@
module Rscons module Rscons
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 Program < Builder class Rscons::Builders::Program < Rscons::Builder
def default_variables(env) def default_variables(env)
{ {
'LD' => nil, 'LD' => nil,
@@ -10,19 +11,19 @@ module Rscons
'LDFLAGS' => [], 'LDFLAGS' => [],
'LIBPATH' => [], 'LIBPATH' => [],
'LIBS' => [], 'LIBS' => [],
'LDCOM' => ['$LD', '-o', '$_TARGET', '$LDFLAGS', '$_SOURCES', '-L$[LIBPATH]', '-l$[LIBS]'] 'LDCMD' => ['${LD}', '-o', '${_TARGET}', '${LDFLAGS}', '${_SOURCES}', '-L${LIBPATH}', '-l${LIBS}']
} }
end end
def run(target, sources, cache, env, vars = {}) def run(target, sources, cache, env, vars)
# build sources to linkable objects # build sources to linkable objects
objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars) objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars)
if objects return false unless objects
ld = if env["LD"] ld = if env["LD"]
env["LD"] env["LD"]
elsif sources.find {|s| s.has_suffix?(env["DSUFFIX"])} elsif sources.find {|s| s.end_with?(*env["DSUFFIX"])}
env["DC"] env["DC"]
elsif sources.find {|s| s.has_suffix?(env["CXXSUFFIX"])} elsif sources.find {|s| s.end_with?(*env["CXXSUFFIX"])}
env["CXX"] env["CXX"]
else else
env["CC"] env["CC"]
@@ -32,7 +33,7 @@ module Rscons
'_SOURCES' => objects, '_SOURCES' => objects,
'LD' => ld, 'LD' => ld,
}) })
command = env.build_command(env['LDCOM'], vars) command = env.build_command(env['LDCMD'], vars)
standard_build("LD #{target}", target, command, objects, env, cache) standard_build("LD #{target}", target, command, objects, env, cache)
end end
end end
+54 -27
View File
@@ -18,9 +18,15 @@ module Rscons
# { # {
# 'fname' => 'program.o', # 'fname' => 'program.o',
# 'checksum' => '87654321', # 'checksum' => '87654321',
# } # },
# ], # ],
# } # 'user_deps' => [
# {
# 'fname' => 'lscript.ld',
# 'checksum' => '77551133',
# },
# ],
# },
# 'program.o' => { # 'program.o' => {
# 'checksum' => '87654321', # 'checksum' => '87654321',
# 'command' => ['gcc', '-c', '-o', 'program.o', 'program.c'], # 'command' => ['gcc', '-c', '-o', 'program.o', 'program.c'],
@@ -32,15 +38,16 @@ module Rscons
# { # {
# 'fname' => 'program.h', # 'fname' => 'program.h',
# 'checksum' => '7979764643', # 'checksum' => '7979764643',
# },
# ],
# '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 #### Constants
@@ -67,26 +74,28 @@ module Rscons
end end
@cache[:targets] ||= {} @cache[:targets] ||= {}
@cache[:directories] ||= {} @cache[:directories] ||= {}
@cache[:version] ||= VERSION
@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
File.open(CACHE_FILE, 'w') do |fh| File.open(CACHE_FILE, 'w') do |fh|
fh.puts(YAML.dump(@cache)) fh.puts(YAML.dump(@cache))
end end
end end
# Check if a target is up to date # Check if target(s) are up to date
# @param target [String] The name of the target file. # @param targets [String, Array] The name(s) of the target file(s).
# @param command [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 options [Hash] Optional options. Can contain the following keys: # @param options [Hash] Optional options. Can contain the following keys:
# :strict_deps:: # :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 value if the target is up to date, meaning that: # @return true if the targets are all up to date, meaning that,
# 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
# - the target's checksum matches its checksum when it was last built # - the target's checksum matches its checksum when it was last built
@@ -96,7 +105,8 @@ module Rscons
# exactly equal to those cached # exactly equal to those cached
# - each cached dependency file's current checksum matches the checksum # - each cached dependency file's current checksum matches the checksum
# stored in the cache file # stored in the cache file
def up_to_date?(target, command, deps, options = {}) def up_to_date?(targets, command, deps, env, options = {})
Array(targets).each do |target|
# target file must exist on disk # target file must exist on disk
return false unless File.exists?(target) return false unless File.exists?(target)
@@ -109,26 +119,38 @@ module Rscons
# 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] == command
cached_deps = @cache[:targets][target][:deps].map { |dc| dc[:fname] } cached_deps = @cache[:targets][target][:deps] || []
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 return false unless deps == cached_deps_fnames
else else
# all dependencies passed in must exist in cache (but cache may have more) # all dependencies passed in must exist in cache (but cache may have more)
return false unless (Set.new(deps) - Set.new(cached_deps)).empty? return false unless (Set.new(deps) - Set.new(cached_deps_fnames)).empty?
end end
# set of user dependencies must match
user_deps = env.get_user_deps(target) || []
cached_user_deps = @cache[:targets][target][:user_deps] || []
cached_user_deps_fnames = cached_user_deps.map { |dc| dc[:fname] }
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
@cache[:targets][target][:deps].map do |dep_cache| (cached_deps + cached_user_deps).each do |dep_cache|
dep_cache[:checksum] == lookup_checksum(dep_cache[:fname]) return false unless dep_cache[:checksum] == lookup_checksum(dep_cache[:fname])
end.all? end
end end
# Store cache information about a target built by a builder true
# @param target [String] The name of the target. end
# @param command [Array] The command used to build the target.
# Store cache information about target(s) built by a builder
# @param targets [String, Array] The name of the target(s) built.
# @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.
def register_build(target, command, deps) # @param env [Environment] The Rscons::Environment.
def register_build(targets, command, deps, env)
Array(targets).each do |target|
@cache[:targets][target.encode(__ENCODING__)] = { @cache[:targets][target.encode(__ENCODING__)] = {
command: command, command: command,
checksum: calculate_checksum(target), checksum: calculate_checksum(target),
@@ -137,8 +159,15 @@ module Rscons
fname: dep.encode(__ENCODING__), fname: dep.encode(__ENCODING__),
checksum: lookup_checksum(dep), checksum: lookup_checksum(dep),
} }
end end,
user_deps: (env.get_user_deps(target) || []).map do |dep|
{
fname: dep.encode(__ENCODING__),
checksum: lookup_checksum(dep),
} }
end,
}
end
end end
# Return a list of targets that have been built # Return a list of targets that have been built
@@ -150,16 +179,14 @@ module Rscons
# 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).each do |i| (0..parts.size-1).each do |i|
subpath = File.join(*parts[0, i + 1]).encode(__ENCODING__) subpath = File.join(*parts[0, i + 1]).encode(__ENCODING__)
unless File.exists?(subpath) unless File.exists?(subpath)
FileUtils.mkdir(subpath) FileUtils.mkdir(subpath)
unless @cache[:directories].include?(subpath)
@cache[:directories][subpath] = true @cache[:directories][subpath] = true
end 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
@@ -179,7 +206,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')).encode(__ENCODING__) rescue ''
end end
end end
end end
+117 -87
View File
@@ -6,36 +6,42 @@ module Rscons
# contains a collection of construction variables, options, builders, and # contains a collection of construction variables, options, builders, and
# rules for building targets. # rules for building targets.
class Environment class Environment
# [Hash] of {"builder_name" => builder_object} pairs. # Hash of +{"builder_name" => builder_object}+ pairs.
attr_reader :builders 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.gsub('\\', '/')
end
# Create an Environment object. # Create an Environment object.
# @param variables [Hash] # @param options [Hash]
# The variables hash can contain construction variables, which are # Possible options keys:
# uppercase strings (such as "CC" or "LDFLAGS"), user variables, which # :echo => :command, :short, or :off (default :short)
# are lowercase strings (such as "sources"), and RScons options, which # :build_root => String specifying build root directory (default nil)
# are lowercase symbols (such as :echo). # :exclude_builders => true to omit adding default builders (default false)
# If a block is given, the Environment object is yielded to the block and # If a block is given, the Environment object is yielded to the block and
# when the block returns, the {#process} method is automatically called. # when the block returns, the {#process} method is automatically called.
def initialize(variables = {}) def initialize(options = {})
@varset = VarSet.new(variables) @varset = VarSet.new
@targets = {} @targets = {}
@user_deps = {}
@builders = {} @builders = {}
@build_dirs = [] @build_dirs = []
@tweakers = [] @build_hooks = []
@varset[:exclude_builders] ||= [] unless options[:exclude_builders]
unless @varset[:exclude_builders] == :all DEFAULT_BUILDERS.each do |builder_class_name|
exclude_builders = Set.new(@varset[:exclude_builders] || []) builder_class = Builders.const_get(builder_class_name)
DEFAULT_BUILDERS.each do |builder_class| builder_class or raise "Could not find builder class #{builder_class_name}"
unless exclude_builders.include?(builder_class.short_name)
add_builder(builder_class.new) add_builder(builder_class.new)
end end
end end
end @echo = options[:echo] || :short
(@varset[:builders] || []).each do |builder| @build_root = options[:build_root]
add_builder(builder)
end
@varset[:echo] ||= :command
if block_given? if block_given?
yield self yield self
@@ -45,17 +51,23 @@ module Rscons
# Make a copy of the Environment object. # Make a copy of the Environment object.
# The cloned environment will contain a copy of all environment options, # The cloned environment will contain a copy of all environment options,
# construction variables, builders, and build directories. It will not # construction variables, and builders (unless :exclude_builders => true is
# contain a copy of the targets or tweakers. # passed as an option). It will not contain a copy of the targets, build
# If a block is given, the Environment object is yielded to the block and # hooks, build directories, or the build root. If a block is given, the
# when the block returns, the {#process} method is automatically called. # Environment object is yielded to the block and when the block returns,
def clone(variables = {}) # the {#process} method is automatically called. The possible options keys
env = Environment.new() # match those documented in the #initialize method.
def clone(options = {})
env = self.class.new(
echo: options[:echo] || @echo,
build_root: options[:build_root],
exclude_builders: true)
unless options[:exclude_builders]
@builders.each do |builder_name, builder| @builders.each do |builder_name, builder|
env.add_builder(builder) env.add_builder(builder)
end end
end
env.append(@varset.clone) env.append(@varset.clone)
env.append(variables)
if block_given? if block_given?
yield env yield env
@@ -66,7 +78,7 @@ module Rscons
# Add a {Builder} object to the Environment. # Add a {Builder} object to the Environment.
def add_builder(builder) def add_builder(builder)
@builders[builder.class.short_name] = builder @builders[builder.name] = builder
var_defs = builder.default_variables(self) var_defs = builder.default_variables(self)
if var_defs if var_defs
var_defs.each_pair do |var, val| var_defs.each_pair do |var, val|
@@ -75,9 +87,9 @@ module Rscons
end end
end end
# Add a tweaker block to the Environment. # Add a build hook to the Environment.
def add_tweaker(&block) def add_build_hook(&block)
@tweakers << block @build_hooks << block
end end
# Specify a build directory for this Environment. # Specify a build directory for this Environment.
@@ -89,18 +101,21 @@ module Rscons
# Return the file name to be built from source_fname with suffix suffix. # Return the file name to be built from source_fname with suffix suffix.
# This method takes into account the Environment's build directories. # This method takes into account the Environment's build directories.
# It also creates any parent directories needed to be able to open and
# write to the output file.
def get_build_fname(source_fname, suffix) def get_build_fname(source_fname, suffix)
build_fname = source_fname.set_suffix(suffix).gsub('\\', '/') build_fname = Rscons.set_suffix(source_fname, suffix).gsub('\\', '/')
@build_dirs.each do |src_dir, obj_dir| found_match = @build_dirs.find do |src_dir, obj_dir|
if src_dir.is_a?(Regexp) if src_dir.is_a?(Regexp)
build_fname.sub!(src_dir, obj_dir) build_fname.sub!(src_dir, obj_dir)
else else
build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/") build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/")
end end
build_fname.gsub!('\\', '/')
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 build_fname
end end
@@ -122,45 +137,42 @@ module Rscons
@varset.send(:append, *args) @varset.send(:append, *args)
end end
# Return a list of target file names
def targets
@targets.keys
end
# Return a list of sources needed to build target target.
def target_sources(target)
@targets[target][:source] rescue nil
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 cache = Cache.new
targets_processed = Set.new targets_processed = {}
process_target = proc do |target| process_target = proc do |target|
if @targets[target][:source].map do |src| targets_processed[target] ||= begin
targets_processed.include?(src) or not @targets.include?(src) or process_target.call(src) @targets[target][:source].each do |src|
end.all? if @targets.include?(src) and not targets_processed.include?(src)
run_builder(@targets[target][:builder], process_target.call(src)
end
end
result = run_builder(@targets[target][:builder],
target, target,
@targets[target][:source], @targets[target][:source],
cache, cache,
@targets[target][:vars] || {}) @targets[target][:vars] || {})
else unless result
false
end
end
@targets.each do |target, info|
next if targets_processed.include?(target)
unless process_target.call(target)
cache.write cache.write
raise BuildError.new("Failed to build #{target}") raise BuildError.new("Failed to build #{target}")
end end
result
end
end
@targets.each do |target, info|
process_target.call(target)
end end
cache.write cache.write
end end
# Clear all targets registered for the Environment.
def clear_targets
@targets = {}
end
# Build a command line from the given template, resolving references to # Build a command line from the given template, resolving references to
# variables using the Environment's construction variables and any extra # variables using the Environment's construction variables and any extra
# variables specified. # variables specified.
@@ -173,22 +185,27 @@ module Rscons
@varset.merge(extra_vars).expand_varref(command_template) @varset.merge(extra_vars).expand_varref(command_template)
end end
# Expand a construction variable reference (String or Array)
def expand_varref(varref)
@varset.expand_varref(varref)
end
# Execute a builder command # Execute a builder command
# @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 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(' ')
end end
if @varset[:echo] == :command if @echo == :command
print_command.call print_command.call
elsif @varset[:echo] == :short elsif @echo == :short
puts short_desc puts short_desc
end end
system(*command, options).tap do |result| system(*command, options).tap do |result|
unless result or @varset[:echo] == :command unless result or @echo == :command
$stdout.write "Failed command was: " $stdout.write "Failed command was: "
print_command.call print_command.call
end end
@@ -214,28 +231,17 @@ module Rscons
end end
end end
# Parse dependencies for a given target from a Makefile. # Manually record a given target as depending on the specified
# This method is used internally by RScons builders. # dependency files.
# @param mf_fname [String] File name of the Makefile to read. def depends(target, *user_deps)
# @param target [String] Name of the target to gather dependencies for. @user_deps[target] ||= []
def parse_makefile_deps(mf_fname, target) @user_deps[target] = (@user_deps[target] + user_deps).uniq
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
end
buildup = '' # Return the list of user dependencies for a given target, or +nil+ for
end # none.
end def get_user_deps(target)
deps @user_deps[target]
end end
# Build a list of source files into files containing one of the suffixes # Build a list of source files into files containing one of the suffixes
@@ -246,9 +252,9 @@ module Rscons
# @param cache [Cache] The Cache. # @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder. # @param vars [Hash] Extra variables to pass to the builder.
# Return a list of the converted file names. # Return a list of the converted file names.
def build_sources(sources, suffixes, cache, vars = {}) def build_sources(sources, suffixes, cache, vars)
sources.map do |source| sources.map do |source|
if source.has_suffix?(suffixes) if source.end_with?(*suffixes)
source source
else else
converted = nil converted = nil
@@ -275,16 +281,40 @@ module Rscons
# Return the result of the builder's run() method. # Return the result of the builder's run() method.
def run_builder(builder, target, sources, cache, vars) def run_builder(builder, target, sources, cache, vars)
vars = @varset.merge(vars) vars = @varset.merge(vars)
@tweakers.each do |tweaker_block| @build_hooks.each do |build_hook_block|
build_operation = { build_operation = {
builder: builder, builder: builder,
target: target, target: target,
sources: sources, sources: sources,
vars: vars, vars: vars,
} }
tweaker_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
# 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
end end
-7
View File
@@ -1,7 +0,0 @@
# Standard Ruby Module class.
class Module
# @return the base module name (not the fully qualified name)
def short_name
name.split(':').last
end
end
-20
View File
@@ -1,20 +0,0 @@
# Standard Ruby String class.
class String
# Check if the given string ends with any of the supplied suffixes
# @param suffix [String, Array] The suffix to look for.
# @return a true value if the string ends with one of the suffixes given.
def has_suffix?(suffix)
if suffix
suffix = [suffix] if suffix.is_a?(String)
suffix = suffix.flatten
suffix.find {|s| self.end_with?(s)}
end
end
# Return a new string with the suffix (dot character and extension) changed
# to the given suffix.
# @param suffix [String] The new suffix.
def set_suffix(suffix = '')
sub(/\.[^.]*$/, suffix)
end
end
+9 -25
View File
@@ -17,20 +17,8 @@ module Rscons
# 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.
# @param type [Symbol, nil] Optional specification of the type desired. def [](key)
# If the variable is a String and type is :array, a 1-element array with @vars[key]
# the variable value will be returned. If the variable is an Array and
# type is :string, the first element from the variable value will be
# returned.
def [](key, type = nil)
val = @vars[key]
if type == :array and not val.is_a?(Array)
[val]
elsif type == :scalar and val.is_a?(Array)
val.first
else
val
end
end end
# Assign a value to a variable. # Assign a value to a variable.
@@ -64,20 +52,16 @@ module Rscons
expand_varref(ent) expand_varref(ent)
end.flatten end.flatten
else else
if varref =~ /^(.*)\$\[(\w+)\](.*)$/ if varref =~ /^(.*)\$\{([^}]+)\}(.*)$/
# expand array with given prefix, suffix
prefix, varname, suffix = $1, $2, $3 prefix, varname, suffix = $1, $2, $3
varval = expand_varref(@vars[varname]) varval = expand_varref(@vars[varname])
unless varval.is_a?(Array) if varval.is_a?(String)
raise "Array expected for $#{varname}" expand_varref("#{prefix}#{varval}#{suffix}")
elsif varval.is_a?(Array)
varval.map {|vv| expand_varref("#{prefix}#{vv}#{suffix}")}.flatten
else
raise "I do not know how to expand a variable reference to a #{varval.class.name} (from #{varname.inspect} => #{@vars[varname].inspect})"
end end
varval.map {|e| "#{prefix}#{e}#{suffix}"}
elsif varref =~ /^\$(.*)$/
# expand a single variable reference
varname = $1
varval = expand_varref(@vars[varname])
varval or raise "Could not find variable #{varname.inspect}"
expand_varref(varval)
else else
varref varref
end end
+1 -1
View File
@@ -1,4 +1,4 @@
module Rscons module Rscons
# gem version # gem version
VERSION = "0.0.14" VERSION = "1.1.0"
end end
+240 -52
View File
@@ -1,45 +1,31 @@
require 'fileutils' require 'fileutils'
describe Rscons do describe Rscons do
BUILD_TEST_RUN_DIR = "build_test_run"
before(:all) do before(:all) do
FileUtils.rm_rf('build_tests_run') FileUtils.rm_rf(BUILD_TEST_RUN_DIR)
@owd = Dir.pwd @owd = Dir.pwd
end end
before(:each) do
@output = ""
$stdout.stub(:write) do |content|
@output += content
end
$stderr.stub(:write) do |content|
@output += content
end
end
after(:each) do after(:each) do
Dir.chdir(@owd) Dir.chdir(@owd)
FileUtils.rm_rf('build_tests_run') FileUtils.rm_rf(BUILD_TEST_RUN_DIR)
end end
def build_testdir(build_script = "build.rb") def test_dir(build_test_directory)
if File.exists?(build_script) FileUtils.cp_r("build_tests/#{build_test_directory}", BUILD_TEST_RUN_DIR)
build_rb = File.read(build_script) Dir.chdir(BUILD_TEST_RUN_DIR)
File.open(build_script, "w") do |fh|
fh.puts(<<EOF + build_rb)
require "simplecov"
SimpleCov.start do
root("#{@owd}")
command_name("build_test_#{@build_test_name}")
add_filter("spec")
end
require "rscons"
EOF
end
IO.popen(%{ruby -I #{@owd}/lib #{build_script}}) do |io|
io.readlines.reject do |line|
line =~ /^Coverage report/
end
end.map(&:strip)
end
end
def test_dir(build_test_directory, build_script = "build.rb")
@build_test_name = build_test_directory
FileUtils.cp_r("build_tests/#{build_test_directory}", 'build_tests_run')
Dir.chdir("build_tests_run")
build_testdir(build_script)
end end
def file_sub(fname) def file_sub(fname)
@@ -53,18 +39,31 @@ EOF
end end
end end
def lines
@output.lines.map(&:rstrip).tap do |v|
@output = ""
end
end
########################################################################### ###########################################################################
# Tests # Tests
########################################################################### ###########################################################################
it 'builds a C program with one source file' do it 'builds a C program with one source file' do
test_dir('simple') test_dir('simple')
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.c'])
end
File.exists?('simple.o').should be_true File.exists?('simple.o').should be_true
`./simple`.should == "This is a simple C program\n" `./simple`.should == "This is a simple C program\n"
end end
it 'prints commands as they are executed' do it 'prints commands as they are executed' do
lines = test_dir('simple') test_dir('simple')
Rscons::Environment.new(echo: :command) do |env|
env["LD"] = "gcc"
env.Program('simple', Dir['*.c'])
end
lines.should == [ lines.should == [
'gcc -c -o simple.o -MMD -MF simple.mf simple.c', 'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
'gcc -o simple simple.o', 'gcc -o simple simple.o',
@@ -72,7 +71,10 @@ EOF
end end
it 'prints short representations of the commands being executed' do it 'prints short representations of the commands being executed' do
lines = test_dir('header') test_dir('header')
Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
lines.should == [ lines.should == [
'CC header.o', 'CC header.o',
'LD header', 'LD header',
@@ -81,61 +83,110 @@ EOF
it 'builds a C program with one source file and one header file' do it 'builds a C program with one source file and one header file' do
test_dir('header') test_dir('header')
Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
File.exists?('header.o').should be_true File.exists?('header.o').should be_true
`./header`.should == "The value is 2\n" `./header`.should == "The value is 2\n"
end end
it 'rebuilds a C module when a header it depends on changes' do it 'rebuilds a C module when a header it depends on changes' do
test_dir('header') test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
`./header`.should == "The value is 2\n" `./header`.should == "The value is 2\n"
file_sub('header.h') {|line| line.sub(/2/, '5')} file_sub('header.h') {|line| line.sub(/2/, '5')}
build_testdir env.process
`./header`.should == "The value is 5\n" `./header`.should == "The value is 5\n"
end end
it 'does not rebuild a C module when its dependencies have not changed' do it 'does not rebuild a C module when its dependencies have not changed' do
lines = test_dir('header') test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
`./header`.should == "The value is 2\n" `./header`.should == "The value is 2\n"
lines.should == [ lines.should == [
'CC header.o', 'CC header.o',
'LD header', 'LD header',
] ]
lines = build_testdir env.process
lines.should == [] lines.should == []
end end
it "does not rebuild a C module when only the file's timestampe has changed" do it "does not rebuild a C module when only the file's timestampe has changed" do
lines = test_dir('header') test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
`./header`.should == "The value is 2\n" `./header`.should == "The value is 2\n"
lines.should == [ lines.should == [
'CC header.o', 'CC header.o',
'LD header', 'LD header',
] ]
file_sub('header.c') {|line| line} file_sub('header.c') {|line| line}
lines = build_testdir env.process
lines.should == [] lines.should == []
end end
it 're-links a program when the link flags have changed' do it 're-links a program when the link flags have changed' do
lines = test_dir('simple') test_dir('simple')
Rscons::Environment.new(echo: :command) do |env|
env.Program('simple', Dir['*.c'])
end
lines.should == [ lines.should == [
'gcc -c -o simple.o -MMD -MF simple.mf simple.c', 'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
'gcc -o simple simple.o', 'gcc -o simple simple.o',
] ]
file_sub('build.rb') {|line| line.sub(/.*CHANGE.FLAGS.*/, ' env["LIBS"] += ["c"]')} Rscons::Environment.new(echo: :command) do |env|
lines = build_testdir env["LIBS"] += ["c"]
env.Program('simple', Dir['*.c'])
end
lines.should == ['gcc -o simple simple.o -lc'] lines.should == ['gcc -o simple simple.o -lc']
end end
it 'builds object files in a different build directory' do it 'builds object files in a different build directory' do
lines = test_dir('build_dir') test_dir('build_dir')
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.Program('build_dir', Dir['src/**/*.c'])
end
`./build_dir`.should == "Hello from two()\n" `./build_dir`.should == "Hello from two()\n"
File.exists?('build_one/one.o').should be_true File.exists?('build_one/one.o').should be_true
File.exists?('build_two/two.o').should be_true File.exists?('build_two/two.o').should be_true
end end
it 'uses build directories before build root' do
test_dir('build_dir')
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir("src", "build")
env.build_root = "build_root"
env.Program('build_dir', Dir['src/**/*.c'])
end
lines.should == ["CC build/one/one.o", "CC build/two/two.o", "LD build_dir"]
end
it 'uses build_root if no build directories match' do
test_dir('build_dir')
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir("src2", "build")
env.build_root = "build_root"
env.Program('build_dir', Dir['src/**/*.c'])
end
lines.should == ["CC build_root/src/one/one.o", "CC build_root/src/two/two.o", "LD build_dir"]
end
it 'cleans built files' do it 'cleans built files' do
lines = test_dir('build_dir') test_dir('build_dir')
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.Program('build_dir', Dir['src/**/*.c'])
end
`./build_dir`.should == "Hello from two()\n" `./build_dir`.should == "Hello from two()\n"
File.exists?('build_one/one.o').should be_true File.exists?('build_one/one.o').should be_true
File.exists?('build_two/two.o').should be_true File.exists?('build_two/two.o').should be_true
@@ -148,7 +199,12 @@ EOF
end end
it 'does not clean created directories if other non-rscons-generated files reside there' do it 'does not clean created directories if other non-rscons-generated files reside there' do
lines = test_dir('build_dir') test_dir('build_dir')
Rscons::Environment.new do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.Program('build_dir', Dir['src/**/*.c'])
end
`./build_dir`.should == "Hello from two()\n" `./build_dir`.should == "Hello from two()\n"
File.exists?('build_one/one.o').should be_true File.exists?('build_one/one.o').should be_true
File.exists?('build_two/two.o').should be_true File.exists?('build_two/two.o').should be_true
@@ -162,14 +218,78 @@ EOF
end end
it 'allows Ruby classes as custom builders to be used to construct files' do it 'allows Ruby classes as custom builders to be used to construct files' do
lines = test_dir('custom_builder') test_dir('custom_builder')
class MySource < Rscons::Builder
def run(target, sources, cache, env, vars)
File.open(target, 'w') do |fh|
fh.puts <<EOF
#define THE_VALUE 5678
EOF
end
target
end
end
Rscons::Environment.new do |env|
env.add_builder(MySource.new)
env.MySource('inc.h', [])
env.Program('program', Dir['*.c'])
end
lines.should == ['CC program.o', 'LD program'] lines.should == ['CC program.o', 'LD program']
File.exists?('inc.h').should be_true File.exists?('inc.h').should be_true
`./program`.should == "The value is 5678\n" `./program`.should == "The value is 5678\n"
end end
it 'supports custom builders with multiple targets' do
test_dir('custom_builder')
class CHGen < Rscons::Builder
def run(target, sources, cache, env, vars)
c_fname = target
h_fname = target.sub(/\.c$/, ".h")
unless cache.up_to_date?([c_fname, h_fname], "", sources, env)
puts "CHGen #{c_fname}"
File.open(c_fname, "w") {|fh| fh.puts "int THE_VALUE = 42;"}
File.open(h_fname, "w") {|fh| fh.puts "extern int THE_VALUE;"}
cache.register_build([c_fname, h_fname], "", sources, env)
end
target
end
end
env = Rscons::Environment.new do |env|
env.add_builder(CHGen.new)
env.CHGen("inc.c", ["program.c"])
env.Program("program", Dir["*.c"] + ["inc.c"])
end
lines.should == ["CHGen inc.c", "CC program.o", "CC inc.o", "LD program"]
File.exists?("inc.c").should be_true
File.exists?("inc.h").should be_true
`./program`.should == "The value is 42\n"
File.open("inc.c", "w") {|fh| fh.puts "int THE_VALUE = 33;"}
env.process
lines.should == ["CHGen inc.c"]
`./program`.should == "The value is 42\n"
end
it 'allows cloning Environment objects' do it 'allows cloning Environment objects' do
lines = test_dir('clone_env') test_dir('clone_env')
debug = Rscons::Environment.new(echo: :command) do |env|
env.build_dir('src', 'debug')
env['CFLAGS'] = '-O2'
env['CPPFLAGS'] = '-DSTRING="Debug Version"'
env.Program('program-debug', Dir['src/*.c'])
end
release = debug.clone do |env|
env["CPPFLAGS"] = '-DSTRING="Release Version"'
env.build_dir('src', 'release')
env.Program('program-release', Dir['src/*.c'])
end
lines.should == [ lines.should == [
%q{gcc -c -o debug/program.o -MMD -MF debug/program.mf '-DSTRING="Debug Version"' -O2 src/program.c}, %q{gcc -c -o debug/program.o -MMD -MF debug/program.mf '-DSTRING="Debug Version"' -O2 src/program.c},
%q{gcc -o program-debug debug/program.o}, %q{gcc -o program-debug debug/program.o},
@@ -180,12 +300,19 @@ EOF
it 'builds a C++ program with one source file' do it 'builds a C++ program with one source file' do
test_dir('simple_cc') test_dir('simple_cc')
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.cc'])
end
File.exists?('simple.o').should be_true File.exists?('simple.o').should be_true
`./simple`.should == "This is a simple C++ program\n" `./simple`.should == "This is a simple C++ program\n"
end end
it 'allows overriding construction variables for individual builder calls' do it 'allows overriding construction variables for individual builder calls' do
lines = test_dir('two_sources') test_dir('two_sources')
Rscons::Environment.new(echo: :command) do |env|
env.Object("one.o", "one.c", 'CPPFLAGS' => ['-DONE'])
env.Program('two_sources', ['one.o', 'two.c'])
end
lines.should == [ lines.should == [
'gcc -c -o one.o -MMD -MF one.mf -DONE one.c', 'gcc -c -o one.o -MMD -MF one.mf -DONE one.c',
'gcc -c -o two.o -MMD -MF two.mf two.c', 'gcc -c -o two.o -MMD -MF two.mf two.c',
@@ -196,7 +323,11 @@ EOF
end end
it 'builds a static library archive' do it 'builds a static library archive' do
lines = test_dir('library') test_dir('library')
Rscons::Environment.new(echo: :command) do |env|
env.Program('library', ['lib.a', 'three.c'])
env.Library("lib.a", ['one.c', 'two.c'], 'CPPFLAGS' => ['-Dmake_lib'])
end
lines.should == [ lines.should == [
'gcc -c -o one.o -MMD -MF one.mf -Dmake_lib one.c', 'gcc -c -o one.o -MMD -MF one.mf -Dmake_lib one.c',
'gcc -c -o two.o -MMD -MF two.mf -Dmake_lib two.c', 'gcc -c -o two.o -MMD -MF two.mf -Dmake_lib two.c',
@@ -208,13 +339,70 @@ EOF
`ar t lib.a`.should == "one.o\ntwo.o\n" `ar t lib.a`.should == "one.o\ntwo.o\n"
end end
it 'supports tweakers to override construction variables' do it 'supports build hooks to override construction variables' do
lines = test_dir("build_dir", "tweaker_build.rb") test_dir("build_dir")
`./tweaker`.should == "Hello from two()\n" Rscons::Environment.new(echo: :command) do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_dir(%r{^src/([^/]+)/}, 'build_\\1/')
env.add_build_hook do |build_op|
if build_op[:target] =~ %r{build_one/.*\.o}
build_op[:vars]["CFLAGS"] << "-O1"
elsif build_op[:target] =~ %r{build_two/.*\.o}
build_op[:vars]["CFLAGS"] << "-O2"
end
end
env.Program('build_hook', Dir['src/**/*.c'])
end
`./build_hook`.should == "Hello from two()\n"
lines.should =~ [ lines.should =~ [
'gcc -c -o build_one/one.o -MMD -MF build_one/one.mf -Isrc/one/ -Isrc/two/ -O1 src/one/one.c', 'gcc -c -o build_one/one.o -MMD -MF build_one/one.mf -Isrc/one/ -Isrc/two/ -O1 src/one/one.c',
'gcc -c -o build_two/two.o -MMD -MF build_two/two.mf -Isrc/one/ -Isrc/two/ -O2 src/two/two.c', 'gcc -c -o build_two/two.o -MMD -MF build_two/two.mf -Isrc/one/ -Isrc/two/ -O2 src/two/two.c',
'gcc -o tweaker build_one/one.o build_two/two.o', 'gcc -o build_hook build_one/one.o build_two/two.o',
] ]
end end
it 'rebuilds when user-specified dependencies change' do
test_dir('simple')
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.c'])
File.open("file.ld", "w") do |fh|
fh.puts("foo")
end
env.depends('simple', 'file.ld')
end
lines.should == ["CC simple.o", "LD simple"]
File.exists?('simple.o').should be_true
`./simple`.should == "This is a simple C program\n"
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.c'])
File.open("file.ld", "w") do |fh|
fh.puts("bar")
end
env.depends('simple', 'file.ld')
end
lines.should == ["LD simple"]
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.c'])
File.unlink("file.ld")
end
lines.should == ["LD simple"]
Rscons::Environment.new do |env|
env.Program('simple', Dir['*.c'])
end
lines.should == []
end
unless ENV["omit_gdc_tests"]
it "supports building D sources" do
test_dir("d")
Rscons::Environment.new(echo: :command) do |env|
env.Program("hello-d", Dir["*.d"])
end
lines.should == [
"gdc -c -o main.o main.d",
"gdc -o hello-d main.o",
]
`./hello-d`.rstrip.should == "Hello from D!"
end
end
end end
+254
View File
@@ -0,0 +1,254 @@
module Rscons
describe Cache do
before do
File.stub(:read) { nil }
end
def build_from(cache)
YAML.should_receive(:load).and_return(cache)
Cache.new
end
describe ".clear" do
it "removes the cache file" do
FileUtils.should_receive(:rm_f).with(Cache::CACHE_FILE)
Cache.clear
end
end
describe "#initialize" do
context "when corrupt" do
it "prints a warning and defaults to an empty hash" do
YAML.should_receive(:load).and_return("string")
$stderr.should_receive(:puts).with(/Warning:.*was.corrupt/)
Cache.new.instance_variable_get(:@cache).is_a?(Hash).should be_true
end
end
end
describe "#write" do
it "should fill in :version and write to file" do
cache = {}
fh = $stdout
fh.should_receive(:puts)
File.should_receive(:open).and_yield(fh)
build_from(cache).write
cache[:version].should == Rscons::VERSION
end
end
describe "#up_to_date?" do
empty_env = "env"
before do
empty_env.stub(:get_user_deps) { nil }
end
it "returns false when target file does not exist" do
File.should_receive(:exists?).with("target").and_return(false)
build_from({}).up_to_date?("target", "command", [], empty_env).should be_false
end
it "returns false when target is not registered in the cache" do
File.should_receive(:exists?).with("target").and_return(true)
build_from({}).up_to_date?("target", "command", [], empty_env).should be_false
end
it "returns false when the target's checksum does not match" do
_cache = {targets: {"target" => {checksum: "abc"}}}
cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("def")
cache.up_to_date?("target", "command", [], empty_env).should be_false
end
it "returns false when the build command has changed" do
_cache = {targets: {"target" => {checksum: "abc", command: "old command"}}}
cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc")
cache.up_to_date?("target", "command", [], empty_env).should be_false
end
it "returns false when there is a new dependency" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1"}]}}}
cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc")
cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env).should be_false
end
it "returns false when a dependency's checksum has changed" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1",
checksum: "dep.1.chk"},
{fname: "dep.2",
checksum: "dep.2.chk"},
{fname: "extra.dep",
checksum: "extra.dep.chk"}],
user_deps: []}}}
cache = build_from(_cache)
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("dep.1").and_return("dep.1.chk")
cache.should_receive(:calculate_checksum).with("dep.2").and_return("dep.2.changed")
cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env).should be_false
end
it "returns false with strict_deps=true when cache has an extra dependency" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1",
checksum: "dep.1.chk"},
{fname: "dep.2",
checksum: "dep.2.chk"},
{fname: "extra.dep",
checksum: "extra.dep.chk"}],
user_deps: []}}}
cache = build_from(_cache)
File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc")
cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env, strict_deps: true).should be_false
end
it "returns false when there is a new user dependency" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1"}],
user_deps: []}}}
cache = build_from(_cache)
env = "env"
env.should_receive(:get_user_deps).with("target").and_return(["file.ld"])
File.should_receive(:exists?).with("target").and_return(true)
cache.should_receive(:calculate_checksum).with("target").and_return("abc")
cache.up_to_date?("target", "command", ["dep.1"], env).should be_false
end
it "returns false when a user dependency checksum has changed" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1",
checksum: "dep.1.chk"},
{fname: "dep.2",
checksum: "dep.2.chk"},
{fname: "extra.dep",
checksum: "extra.dep.chk"}],
user_deps: [{fname: "user.dep",
checksum: "user.dep.chk"}]}}}
cache = build_from(_cache)
env = "env"
env.should_receive(:get_user_deps).with("target").and_return(["user.dep"])
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("dep.1").and_return("dep.1.chk")
cache.should_receive(:calculate_checksum).with("dep.2").and_return("dep.2.chk")
cache.should_receive(:calculate_checksum).with("extra.dep").and_return("extra.dep.chk")
cache.should_receive(:calculate_checksum).with("user.dep").and_return("INCORRECT")
cache.up_to_date?("target", "command", ["dep.1", "dep.2"], env).should be_false
end
it "returns true when no condition for false is met" do
_cache = {targets: {"target" => {checksum: "abc",
command: "command",
deps: [{fname: "dep.1",
checksum: "dep.1.chk"},
{fname: "dep.2",
checksum: "dep.2.chk"},
{fname: "extra.dep",
checksum: "extra.dep.chk"}],
user_deps: []}}}
cache = build_from(_cache)
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("dep.1").and_return("dep.1.chk")
cache.should_receive(:calculate_checksum).with("dep.2").and_return("dep.2.chk")
cache.should_receive(:calculate_checksum).with("extra.dep").and_return("extra.dep.chk")
cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env).should be_true
end
end
describe "#register_build" do
it "stores the given information in the cache" do
_cache = {}
cache = build_from(_cache)
env = "env"
env.should_receive(:get_user_deps).with("the target").and_return(["user.dep"])
cache.should_receive(:calculate_checksum).with("the target").and_return("the checksum")
cache.should_receive(:calculate_checksum).with("dep 1").and_return("dep 1 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.register_build("the target", "the command", ["dep 1", "dep 2"], env)
cached_target = cache.instance_variable_get(:@cache)[:targets]["the target"]
cached_target.should_not be_nil
cached_target[:command].should == "the command"
cached_target[:checksum].should == "the checksum"
cached_target[:deps].should == [
{fname: "dep 1", checksum: "dep 1 checksum"},
{fname: "dep 2", checksum: "dep 2 checksum"},
]
cached_target[:user_deps].should == [
{fname: "user.dep", checksum: "user.dep checksum"},
]
end
end
describe "#targets" do
it "returns a list of targets that are cached" do
cache = {targets: {"t1" => {}, "t2" => {}, "t3" => {}}}
build_from(cache).targets.should == ["t1", "t2", "t3"]
end
end
describe "#mkdir_p" do
it "makes directories and records any created in the cache" 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")
File.should_receive(:exists?).with("one/two/three").and_return(false)
FileUtils.should_receive(:mkdir).with("one/two/three")
File.should_receive(:exists?).with("one").and_return(true)
File.should_receive(:exists?).with("one/two").and_return(true)
File.should_receive(:exists?).with("one/two/four").and_return(false)
FileUtils.should_receive(:mkdir).with("one/two/four")
cache.mkdir_p("one/two/three")
cache.mkdir_p("one\\two\\four")
cache.directories.should == ["one/two", "one/two/three", "one/two/four"]
end
end
describe "#directories" do
it "returns a list of directories that are cached" do
_cache = {directories: {"dir1" => true, "dir2" => true}}
build_from(_cache).directories.should == ["dir1", "dir2"]
end
end
describe "#lookup_checksum" do
it "does not re-calculate the checksum when it is already cached" do
cache = build_from({})
cache.instance_variable_set(:@lookup_checksums, {"f1" => "f1.chk"})
cache.should_not_receive(:calculate_checksum)
cache.send(:lookup_checksum, "f1").should == "f1.chk"
end
it "calls calculate_checksum when the checksum is not cached" do
cache = build_from({})
cache.should_receive(:calculate_checksum).with("f1").and_return("ck")
cache.send(:lookup_checksum, "f1").should == "ck"
end
end
describe "#calculate_checksum" do
it "calculates the MD5 of the file contents" do
contents = "contents"
File.should_receive(:read).with("fname", mode: "rb").and_return(contents)
Digest::MD5.should_receive(:hexdigest).with(contents).and_return("the_checksum")
build_from({}).send(:calculate_checksum, "fname").should == "the_checksum"
end
end
end
end
+314 -6
View File
@@ -1,6 +1,30 @@
module Rscons module Rscons
describe Environment do describe Environment do
describe '.clone' do describe "#initialize" do
it "adds the default builders when they are not excluded" do
env = Environment.new
env.builders.size.should be > 0
env.builders.map {|name, builder| builder.is_a?(Builder)}.all?.should be_true
env.builders.find {|name, builder| name == "Object"}.should_not be_nil
env.builders.find {|name, builder| name == "Program"}.should_not be_nil
env.builders.find {|name, builder| name == "Library"}.should_not be_nil
end
it "excludes the default builders with exclude_builders: :all" do
env = Environment.new(exclude_builders: true)
env.builders.size.should == 0
end
context "when a block is given" do
it "yields self and invokes #process()" do
env = Environment.new do |env|
env.should_receive(:process)
end
end
end
end
describe "#clone" do
it 'should create unique copies of each construction variable' do it 'should create unique copies of each construction variable' do
env = Environment.new env = Environment.new
env["CPPPATH"] << "path1" env["CPPPATH"] << "path1"
@@ -9,15 +33,300 @@ module Rscons
env["CPPPATH"].should == ["path1"] env["CPPPATH"].should == ["path1"]
env2["CPPPATH"].should == ["path1", "path2"] env2["CPPPATH"].should == ["path1", "path2"]
end end
context "when a block is given" do
it "yields self and invokes #process()" do
env = Environment.new
env.clone do |env2|
env2.should_receive(:process)
end
end
end
end end
describe '.parse_makefile_deps' do describe "#add_builder" do
it "adds the builder to the list of builders" do
env = Environment.new(exclude_builders: true)
env.builders.keys.should == []
env.add_builder(Rscons::Builders::Object.new)
env.builders.keys.should == ["Object"]
end
end
describe "#get_build_fname" do
context "with no build directories" do
it "returns the name of the source file with suffix changed" do
env = Environment.new
env.get_build_fname("src/dir/file.c", ".o").should == "src/dir/file.o"
env.get_build_fname("src\\dir\\other.d", ".a").should == "src/dir/other.a"
env.get_build_fname("source.cc", ".o").should == "source.o"
end
context "with a build_root" do
it "uses the build_root unless the path is absolute" do
env = Environment.new
env.build_root = "build/proj"
env.get_build_fname("src/dir/file.c", ".o").should == "build/proj/src/dir/file.o"
env.get_build_fname("/some/lib.c", ".a").should == "/some/lib.a"
env.get_build_fname("C:\\abspath\\mod.cc", ".o").should == "C:/abspath/mod.o"
env.get_build_fname("build\\proj\\generated.c", ".o").should == "build/proj/generated.o"
env.get_build_fname("build/proj.XX", ".yy").should == "build/proj/build/proj.yy"
end
end
end
context "with build directories" do
it "uses the build directories to create the output file name" do
env = Environment.new
env.build_dir("src", "bld")
env.build_dir(%r{^libs/([^/]+)}, 'build/libs/\1')
env.get_build_fname("src/input.cc", ".o").should == "bld/input.o"
env.get_build_fname("libs/lib1/some/file.c", ".o").should == "build/libs/lib1/some/file.o"
env.get_build_fname("libs/otherlib/otherlib.cc", ".o").should == "build/libs/otherlib/otherlib.o"
env.get_build_fname("other_directory/o.d", ".a").should == "other_directory/o.a"
end
context "with a build_root" do
it "uses the build_root unless a build directory matches or the path is absolute" do
env = Environment.new
env.build_dir("src", "bld")
env.build_dir(%r{^libs/([^/]+)}, 'build/libs/\1')
env.build_root = "bldit"
env.get_build_fname("src/input.cc", ".o").should == "bld/input.o"
env.get_build_fname("libs/lib1/some/file.c", ".o").should == "build/libs/lib1/some/file.o"
env.get_build_fname("libs/otherlib/otherlib.cc", ".o").should == "build/libs/otherlib/otherlib.o"
env.get_build_fname("other_directory/o.d", ".a").should == "bldit/other_directory/o.a"
env.get_build_fname("bldit/some/mod.d", ".a").should == "bldit/some/mod.a"
end
end
end
end
describe "#[]" do
it "allows reading construction variables" do
env = Environment.new
env["CFLAGS"] = ["-g", "-Wall"]
env["CFLAGS"].should == ["-g", "-Wall"]
end
end
describe "#[]=" do
it "allows writing construction variables" do
env = Environment.new
env["CFLAGS"] = ["-g", "-Wall"]
env["CFLAGS"] -= ["-g"]
env["CFLAGS"] += ["-O3"]
env["CFLAGS"].should == ["-Wall", "-O3"]
env["other_var"] = "val33"
env["other_var"].should == "val33"
end
end
describe "#append" do
it "allows adding many construction variables at once" do
env = Environment.new
env["CFLAGS"] = ["-g"]
env["CPPPATH"] = ["inc"]
env.append("CFLAGS" => ["-Wall"], "CPPPATH" => ["include"])
env["CFLAGS"].should == ["-Wall"]
env["CPPPATH"].should == ["include"]
end
end
describe "#process" do
it "runs builders for all of the targets specified" do
env = Environment.new
env.Program("a.out", "main.c")
cache = "cache"
Cache.should_receive(:new).and_return(cache)
env.should_receive(:run_builder).with(anything, "a.out", ["main.c"], cache, {}).and_return(true)
cache.should_receive(:write)
env.process
end
it "builds dependent targets first" do
env = Environment.new
env.Program("a.out", "main.o")
env.Object("main.o", "other.cc")
cache = "cache"
Cache.should_receive(:new).and_return(cache)
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")
cache.should_receive(:write)
env.process
end
it "raises a BuildError when building fails" do
env = Environment.new
env.Program("a.out", "main.o")
env.Object("main.o", "other.cc")
cache = "cache"
Cache.should_receive(:new).and_return(cache)
env.should_receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return(false)
cache.should_receive(:write)
expect { env.process }.to raise_error BuildError, /Failed.to.build.main.o/
end
end
describe "#clear_targets" do
it "resets @targets to an empty hash" do
env = Environment.new
env.Program("a.out", "main.o")
expect(env.instance_variable_get(:@targets).keys).to eq(["a.out"])
env.clear_targets
expect(env.instance_variable_get(:@targets).keys).to eq([])
end
end
describe "#build_command" do
it "returns a command based on the variables in the Environment" do
env = Environment.new
env["path"] = ["dir1", "dir2"]
env["flags"] = ["-x", "-y", "${specialflag}"]
env["specialflag"] = "-z"
template = ["cmd", "-I${path}", "${flags}", "${_source}", "${_dest}"]
cmd = env.build_command(template, "_source" => "infile", "_dest" => "outfile")
cmd.should == ["cmd", "-Idir1", "-Idir2", "-x", "-y", "-z", "infile", "outfile"]
end
end
describe "#expand_varref" do
it "returns the fully expanded variable reference" do
env = Environment.new
env["path"] = ["dir1", "dir2"]
env["flags"] = ["-x", "-y", "${specialflag}"]
env["specialflag"] = "-z"
env.expand_varref(["-p${path}", "${flags}"]).should == ["-pdir1", "-pdir2", "-x", "-y", "-z"]
env.expand_varref("foo").should == "foo"
expect {env.expand_varref("${foo}")}.to raise_error /expand.a.variable.reference/
env.expand_varref("${specialflag}").should == "-z"
env.expand_varref("${path}").should == ["dir1", "dir2"]
end
end
describe "#execute" do
context "with echo: :short" do
context "with no errors" do
it "prints the short description and executes the command" do
env = Environment.new(echo: :short)
env.should_receive(:puts).with("short desc")
env.should_receive(:system).with("a", "command", {}).and_return(true)
env.execute("short desc", ["a", "command"])
end
end
context "with errors" do
it "prints the short description, executes the command, and prints the failed command line" do
env = Environment.new(echo: :short)
env.should_receive(:puts).with("short desc")
env.should_receive(:system).with("a", "command", {}).and_return(false)
$stdout.should_receive(:write).with("Failed command was: ")
env.should_receive(:puts).with("a command")
env.execute("short desc", ["a", "command"])
end
end
end
context "with echo: :command" do
it "prints the command executed and executes the command" do
env = Environment.new(echo: :command)
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.execute("short desc", ["a", "command", "--arg=val with spaces"], opt: :val)
end
end
end
describe "#method_missing" do
it "calls the original method missing when the target method is not a known builder" do
env = Environment.new
env.should_receive(:orig_method_missing).with(:foobar)
env.foobar
end
it "records the target when the target method is a known builder" do
env = Environment.new
env.instance_variable_get(:@targets).should == {}
env.Program("target", ["src1", "src2"], var: "val")
target = env.instance_variable_get(:@targets)["target"]
target.should_not be_nil
target[:builder].is_a?(Builder).should be_true
target[:source].should == ["src1", "src2"]
target[:vars].should == {var: "val"}
target[:args].should == []
end
it "raises an error when vars is not a Hash" do
env = Environment.new
expect { env.Program("a.out", "main.c", "other") }.to raise_error /Unexpected construction variable set/
end
end
describe "#depends" do
it "records the given dependencies in @user_deps" do
env = Environment.new
env.depends("foo", "bar", "baz")
env.instance_variable_get(:@user_deps).should == {"foo" => ["bar", "baz"]}
end
it "records user dependencies only once" do
env = Environment.new
env.instance_variable_set(:@user_deps, {"foo" => ["bar"]})
env.depends("foo", "bar", "baz")
env.instance_variable_get(:@user_deps).should == {"foo" => ["bar", "baz"]}
end
end
describe "#build_sources" do
class ABuilder < Builder
def produces?(target, source, env)
target =~ /\.ab_out$/ and source =~ /\.ab_in$/
end
end
it "finds and invokes a builder to produce output files with the requested suffixes" do
cache = "cache"
env = Environment.new
env.add_builder(ABuilder.new)
env.builders["Object"].should_receive(:run).with("mod.o", ["mod.c"], cache, env, anything).and_return("mod.o")
env.builders["ABuilder"].should_receive(:run).with("mod2.ab_out", ["mod2.ab_in"], cache, env, anything).and_return("mod2.ab_out")
env.build_sources(["precompiled.o", "mod.c", "mod2.ab_in"], [".o", ".ab_out"], cache, {}).should == ["precompiled.o", "mod.o", "mod2.ab_out"]
end
end
describe "#run_builder" do
it "modifies the construction variables using given build hooks and invokes the builder" do
env = Environment.new
env.add_build_hook do |build_op|
if build_op[:sources].first =~ %r{src/special}
build_op[:vars]["CFLAGS"] += ["-O3", "-DSPECIAL"]
end
end
env.builders["Object"].stub(:run) do |target, sources, cache, env, vars|
vars["CFLAGS"].should == []
end
env.run_builder(env.builders["Object"], "build/normal/module.o", ["src/normal/module.c"], "cache", {})
env.builders["Object"].stub(:run) do |target, sources, cache, env, vars|
vars["CFLAGS"].should == ["-O3", "-DSPECIAL"]
end
env.run_builder(env.builders["Object"], "build/special/module.o", ["src/special/module.c"], "cache", {})
end
end
describe ".parse_makefile_deps" do
it 'handles dependencies on one line' do it 'handles dependencies on one line' do
File.should_receive(:read).with('makefile').and_return(<<EOS) File.should_receive(:read).with('makefile').and_return(<<EOS)
module.o: source.cc module.o: source.cc
EOS EOS
env = Environment.new Environment.parse_makefile_deps('makefile', 'module.o').should == ['source.cc']
env.parse_makefile_deps('makefile', 'module.o').should == ['source.cc']
end end
it 'handles dependencies split across many lines' do it 'handles dependencies split across many lines' do
@@ -26,8 +335,7 @@ module.o: module.c \\
module.h \\ module.h \\
other.h other.h
EOS EOS
env = Environment.new Environment.parse_makefile_deps('makefile', 'module.o').should == [
env.parse_makefile_deps('makefile', 'module.o').should == [
'module.c', 'module.h', 'other.h'] 'module.c', 'module.h', 'other.h']
end end
end end
-6
View File
@@ -1,6 +0,0 @@
describe "Module#short_name" do
it "returns the inner name of the module" do
Rscons::Environment.short_name.should == "Environment"
Rscons::Object.short_name.should == "Object"
end
end
+16 -17
View File
@@ -30,12 +30,6 @@ module Rscons
v["qax"].should == [3, 6] v["qax"].should == [3, 6]
v["qux"].should == {a: :b} v["qux"].should == {a: :b}
end end
it "allows accessing a non-array converted to an array" do
v["fuz", :array].should == ["a string"]
end
it "allows accessing an array as a single value" do
v["qax", :scalar].should == 3
end
end end
describe :[]= do describe :[]= do
@@ -84,30 +78,35 @@ module Rscons
v = VarSet.new("CFLAGS" => ["-Wall", "-O2"], v = VarSet.new("CFLAGS" => ["-Wall", "-O2"],
"CC" => "gcc", "CC" => "gcc",
"CPPPATH" => ["dir1", "dir2"], "CPPPATH" => ["dir1", "dir2"],
"compiler" => "$CC", "compiler" => "${CC}",
"cmd" => ["$CC", "-c", "$CFLAGS", "-I$[CPPPATH]"]) "cmd" => ["${CC}", "-c", "${CFLAGS}", "-I${CPPPATH}"])
it "expands to the string itself if the string is not a variable reference" do it "expands to the string itself if the string is not a variable reference" do
v.expand_varref("CC").should == "CC" v.expand_varref("CC").should == "CC"
v.expand_varref("CPPPATH").should == "CPPPATH" v.expand_varref("CPPPATH").should == "CPPPATH"
v.expand_varref("str").should == "str" v.expand_varref("str").should == "str"
end end
it "expands a single variable reference beginning with a '$'" do it "expands a single variable reference beginning with a '$'" do
v.expand_varref("$CC").should == "gcc" v.expand_varref("${CC}").should == "gcc"
v.expand_varref("$CPPPATH").should == ["dir1", "dir2"] v.expand_varref("${CPPPATH}").should == ["dir1", "dir2"]
end end
it "expands a single variable reference in $[arr] notation" do it "expands a single variable reference in ${arr} notation" do
v.expand_varref("prefix$[CFLAGS]suffix").should == ["prefix-Wallsuffix", "prefix-O2suffix"] v.expand_varref("prefix${CFLAGS}suffix").should == ["prefix-Wallsuffix", "prefix-O2suffix"]
v.expand_varref(v["cmd"]).should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"] v.expand_varref(v["cmd"]).should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"]
end end
it "expands a variable reference recursively" do it "expands a variable reference recursively" do
v.expand_varref("$compiler").should == "gcc" v.expand_varref("${compiler}").should == "gcc"
v.expand_varref("$cmd").should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"] v.expand_varref("${cmd}").should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"]
end end
it "raises an error when array notation is applied to a non-array variable" do it "resolves multiple variable references in one element by enumerating all combinations" do
expect { v.expand_varref("$[CC]") }.to raise_error /Array.expected/ v.expand_varref("cflag: ${CFLAGS}, cpppath: ${CPPPATH}, compiler: ${compiler}").should == [
"cflag: -Wall, cpppath: dir1, compiler: gcc",
"cflag: -O2, cpppath: dir1, compiler: gcc",
"cflag: -Wall, cpppath: dir2, compiler: gcc",
"cflag: -O2, cpppath: dir2, compiler: gcc",
]
end end
it "raises an error when a variable reference refers to a non-existent variable" do it "raises an error when a variable reference refers to a non-existent variable" do
expect { v.expand_varref("$not_here") }.to raise_error /Could.not.find.variable..not_here/ expect { v.expand_varref("${not_here}") }.to raise_error /I do not know how to expand a variable reference to a NilClass/
end end
end end
end end
+26
View File
@@ -0,0 +1,26 @@
describe Rscons do
describe ".clean" do
it "removes all build targets and created directories" do
cache = "cache"
Rscons::Cache.should_receive(:new).and_return(cache)
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/main.o")
cache.should_receive(:directories).and_return(["build/one", "build/one/two", "build", "other"])
File.should_receive(:directory?).with("build/one/two").and_return(true)
Dir.should_receive(:entries).with("build/one/two").and_return([".", ".."])
Dir.should_receive(:rmdir).with("build/one/two")
File.should_receive(:directory?).with("build/one").and_return(true)
Dir.should_receive(:entries).with("build/one").and_return([".", ".."])
Dir.should_receive(:rmdir).with("build/one")
File.should_receive(:directory?).with("build").and_return(true)
Dir.should_receive(:entries).with("build").and_return([".", ".."])
Dir.should_receive(:rmdir).with("build")
File.should_receive(:directory?).with("other").and_return(true)
Dir.should_receive(:entries).with("other").and_return([".", "..", "other.file"])
Rscons::Cache.should_receive(:clear)
Rscons.clean
end
end
end
+3 -1
View File
@@ -1,5 +1,7 @@
require "simplecov" require "simplecov"
SimpleCov.start SimpleCov.start do
add_filter "/spec/"
end
require "rscons" require "rscons"