Compare commits

..
67 Commits
Author SHA1 Message Date
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
josh 338a49ba9c v0.0.14 2013-10-29 16:18:14 -04:00
josh c23bb8c5e0 add support for building D sources (defaulting to gdc) 2013-10-29 16:17:51 -04:00
josh 31abeed3d5 v0.0.13 2013-10-15 16:29:54 -04:00
josh 446fef931f add "tweakers" functionality
tweakers allow the user to modify construction variables before any build is performed
2013-10-15 16:15:37 -04:00
josh 4a2997feec VarSet: make a deep copy of values passed to VarSet#append() 2013-10-15 16:10:56 -04:00
josh 013995bbc0 refactor into Environment#run_builder() 2013-10-15 13:36:25 -04:00
josh cd266c511e v0.0.12 2013-10-08 11:54:38 -04:00
josh 0f5fee359d cloning an Environment should make a deep copy of its construction variables 2013-10-08 11:54:24 -04:00
josh dc6bac2895 change build_dir build test to test build_dir specified as a Regexp 2013-10-04 21:40:11 -04:00
josh e50aac31c9 tweak custom_builder build test for additional coverage 2013-10-04 21:26:51 -04:00
josh e788f6ec25 claim simplecov coverage from build_tests 2013-10-04 21:05:44 -04:00
josh 8d893d7b72 rspec Module monkey-patching 2013-10-04 20:22:37 -04:00
josh c258dc379f rspec VarSet 2013-10-04 20:18:40 -04:00
josh 0facfb274d turn on simplecov 2013-10-04 19:37:56 -04:00
josh 3339cf5280 Cache: re-encode file names in default encoding 2013-09-20 15:58:55 -04:00
josh e041ea6612 v0.0.11 2013-09-19 17:06:37 -04:00
josh ab51ddcc1b do not try to clean directories that have already been removed 2013-09-19 17:06:20 -04:00
josh 3e4e9cce1c v0.0.10 2013-09-19 16:48:15 -04:00
josh 32d19489cb Cache: encode directory names in __ENCODING__ - close #3 2013-09-19 16:45:35 -04:00
josh 45a0d56c54 recover from, and warn when, .rsconscache loads something other than a Hash 2013-09-19 16:18:07 -04:00
josh 9b954d9d98 v0.0.9 2013-09-19 09:43:17 -04:00
josh e63b4c932f Cache: create missing hash entries from previous versions of the cache file 2013-09-19 09:43:00 -04:00
josh 97e11f45e5 v0.0.8 2013-09-18 21:18:00 -04:00
josh 5c6239ea6d Rscons.clean() removes any directories it created if they are empty 2013-09-18 21:17:42 -04:00
josh 770a22250a add Rscons.clear() class method 2013-09-17 21:48:50 -04:00
josh 983cb7cf21 v0.0.7 2013-09-16 23:03:14 -04:00
josh 986b8f08cc prepend builder-temporary construction variables with "_"; reserve lowercase string variables for user 2013-09-16 22:30:16 -04:00
josh f13664331f create requisite output directories for build targets; refactor common buile functionality into Builder#standard_build() 2013-09-15 15:27:16 -04:00
josh 17c7bf1d31 v0.0.6 2013-09-12 23:37:37 -04:00
josh 64d15602cc add Library default builder 2013-09-12 23:35:34 -04:00
josh e04e67698a add rspec test for overridding construction variables for an individual builer 2013-09-12 23:05:04 -04:00
josh 837dff9374 pass extra construction variables to Builder#run() 2013-09-12 22:57:22 -04:00
josh 75f2418570 refactor some Program functionality into Environment#build_sources() 2013-09-12 22:38:52 -04:00
josh 711e96cec8 write out cache file when raising a build error exception 2013-08-12 14:40:46 -04:00
josh 289d27d41d raise Rscons::BuildError on a build failure 2013-08-12 13:13:22 -04:00
josh 052b1afe19 v0.0.3 2013-08-05 17:19:22 -04:00
josh 72976be765 fix verifying a target's checksum 2013-08-05 17:19:08 -04:00
31 changed files with 1275 additions and 225 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
+129 -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,135 @@ 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
```ruby
class GenerateFoo < Rscons::Builder
def run(target, sources, cache, env, vars)
File.open(target, "w") do |fh|
fh.puts <<EOF
#define GENERATED 42
EOF
end end
end
end
Rscons::Environment.new do |env|
env.GenerateFoo("foo.h", [])
env.Program("a.out", Dir["*.c"])
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
```
### 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 (or select)
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
* a collection of targets to build
* a collection of build hooks
When cloning an environment, the construction variables, builders, and build
directories are cloned, but the new environment does not inherit any of the
targets or build hooks 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 Variables
The default construction variables that Rscons uses are named using uppercase
strings.
Rscons options are lowercase symbols.
Lowercase strings are reserved as user-defined construction variables.
## 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('src', 'build')
env.Program('build_dir', 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
-16
View File
@@ -1,16 +0,0 @@
class MySource < Rscons::Builder
def run(target, sources, cache, env)
File.open(target, 'w') do |fh|
fh.puts <<EOF
#define THE_VALUE 5678
EOF
end
target
end
end
Rscons::Environment.new(echo: :short) do |env|
env.add_builder(MySource.new)
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
+8
View File
@@ -0,0 +1,8 @@
#include <stdio.h>
#ifdef make_lib
int main(int argc, char *argv[])
{
printf("Library\n");
}
#endif
View File
Whitespace-only changes.
View File
Whitespace-only changes.
-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
+8
View File
@@ -0,0 +1,8 @@
#include <stdio.h>
#ifdef ONE
int main(int argc, char *argv[])
{
printf("This is a C program with two sources.\n");
}
#endif
+3
View File
@@ -0,0 +1,3 @@
#ifdef ONE
#error ONE should not be defined
#endif
+22
View File
@@ -8,13 +8,35 @@ require "rscons/monkey/module"
require "rscons/monkey/string" require "rscons/monkey/string"
# default builders # default builders
require "rscons/builders/library"
require "rscons/builders/object" require "rscons/builders/object"
require "rscons/builders/program" require "rscons/builders/program"
# Namespace module for rscons classes # Namespace module for rscons classes
module Rscons module Rscons
DEFAULT_BUILDERS = [ DEFAULT_BUILDERS = [
Library,
Object, Object,
Program, Program,
] ]
class BuildError < Exception
end
# Remove all generated files
def self.clean
cache = Cache.new
# remove all built files
cache.targets.each do |target|
FileUtils.rm_f(target)
end
# remove all created directories if they are empty
cache.directories.sort {|a, b| b.size <=> a.size}.each do |directory|
next unless File.directory?(directory)
if (Dir.entries(directory) - ['.', '..']).empty?
Dir.rmdir(directory) rescue nil
end
end
Cache.clear
end
end end
+15
View File
@@ -1,3 +1,5 @@
require "fileutils"
module Rscons module Rscons
# 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
@@ -16,5 +18,18 @@ module Rscons
def produces?(target, source, env) def produces?(target, source, env)
false false
end end
# Check if the cache is up to date for the target and if not execute the
# build command.
# Return the name of the target or false on failure.
def standard_build(short_cmd_string, target, command, sources, env, cache)
unless cache.up_to_date?(target, command, sources)
cache.mkdir_p(File.dirname(target))
FileUtils.rm_f(target)
return false unless env.execute(short_cmd_string, command)
cache.register_build(target, command, sources)
end
target
end
end end
end end
+28
View File
@@ -0,0 +1,28 @@
require 'fileutils'
module Rscons
# A default RScons builder that produces a static library archive.
class Library < Builder
def default_variables(env)
{
'AR' => 'ar',
'LIBSUFFIX' => '.a',
'ARFLAGS' => [],
'ARCMD' => ['${AR}', 'rcs', '${ARFLAGS}', '${_TARGET}', '${_SOURCES}']
}
end
def run(target, sources, cache, env, vars = {})
# build sources to linkable objects
objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars)
if objects
vars = vars.merge({
'_TARGET' => target,
'_SOURCES' => objects,
})
command = env.build_command(env['ARCMD'], vars)
standard_build("AR #{target}", target, command, objects, env, cache)
end
end
end
end
+42 -32
View File
@@ -2,17 +2,24 @@ module Rscons
# 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,46 +27,49 @@ 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',
'DFLAGS' => [],
'DSUFFIX' => '.d',
'D_IMPORT_PATH' => [],
'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.has_suffix?(env['OBJSUFFIX']) and KNOWN_SUFFIXES.find do |compiler, suffix_var|
source.has_suffix?(env['ASSUFFIX']) or source.has_suffix?(env[suffix_var])
source.has_suffix?(env['CSUFFIX']) or end
source.has_suffix?(env['CXXSUFFIX']))
end end
def run(target, sources, cache, env) def run(target, sources, cache, env, vars = {})
vars = { vars = vars.merge({
'TARGET' => target, '_TARGET' => target,
'SOURCES' => sources, '_SOURCES' => sources,
'DEPFILE' => target.set_suffix('.mf'), '_DEPFILE' => target.set_suffix('.mf'),
} })
com_prefix = if sources.first.has_suffix?(env['ASSUFFIX']) com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var|
'AS' sources.first.has_suffix?(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)
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) unless cache.up_to_date?(target, command, sources)
cache.mkdir_p(File.dirname(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)
end end
+20 -30
View File
@@ -10,39 +10,29 @@ 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) def run(target, sources, cache, env, vars = {})
# convert sources to object file names # build sources to linkable objects
objects = sources.map do |source| objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars)
if source.has_suffix?([env['OBJSUFFIX'], env['LIBSUFFIX']]) ld = if env["LD"]
source env["LD"]
else elsif sources.find {|s| s.has_suffix?(env["DSUFFIX"])}
o_file = env.get_build_fname(source, env['OBJSUFFIX', :string]) env["DC"]
builder = env.builders.values.find { |b| b.produces?(o_file, source, env) } elsif sources.find {|s| s.has_suffix?(env["CXXSUFFIX"])}
builder or raise "No builder found to convert input source #{source.inspect} to an object file." env["CXX"]
builder.run(o_file, [source], cache, env) or break else
end env["CC"]
end end
if objects vars = vars.merge({
use_cxx = sources.map do |s| '_TARGET' => target,
s.has_suffix?(env['CXXSUFFIX']) '_SOURCES' => objects,
end.any? 'LD' => ld,
ld_alt = use_cxx ? env['CXX'] : env['CC'] })
vars = { command = env.build_command(env['LDCMD'], vars)
'TARGET' => target, standard_build("LD #{target}", target, command, objects, env, cache)
'SOURCES' => objects,
'LD' => env['LD'] || ld_alt,
}
command = env.build_command(env['LDCOM'], vars)
unless cache.up_to_date?(target, command, objects)
return false unless env.execute("LD #{target}", command)
cache.register_build(target, command, objects)
end
target
end
end end
end end
end end
+48 -10
View File
@@ -36,6 +36,11 @@ module Rscons
# ] # ]
# } # }
# } # }
# directories: {
# 'build' => true,
# 'build/one' => true,
# 'build/two' => true,
# }
# } # }
class Cache class Cache
#### Constants #### Constants
@@ -55,15 +60,19 @@ module Rscons
# 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 { @cache = YAML.load(File.read(CACHE_FILE)) rescue {}
targets: {}, unless @cache.is_a?(Hash)
version: VERSION, $stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
} @cache = {}
end
@cache[:targets] ||= {}
@cache[:directories] ||= {}
@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
@@ -80,6 +89,7 @@ module Rscons
# @return true value if the target is up to date, meaning that: # @return true value if the target is up to date, meaning that:
# - 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 command used to build the target is the same as last time # - the command used to build the target is the same as last time
# - all dependencies listed are also listed in the cache, or, if # - all dependencies listed are also listed in the cache, or, if
# :strict_deps was given in options, the list of dependencies is # :strict_deps was given in options, the list of dependencies is
@@ -93,6 +103,9 @@ module Rscons
# 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
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] == command
@@ -106,9 +119,11 @@ module Rscons
end end
# all cached dependencies must have their checksums match # all cached dependencies must have their checksums match
@cache[:targets][target][:deps].map do |dep_cache| @cache[:targets][target][: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
true
end end
# Store cache information about a target built by a builder # Store cache information about a target built by a builder
@@ -116,18 +131,41 @@ module Rscons
# @param command [Array] The command used to build the target. # @param command [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) def register_build(target, command, deps)
@cache[:targets][target] = { @cache[:targets][target.encode(__ENCODING__)] = {
command: command, command: command,
checksum: calculate_checksum(target), checksum: calculate_checksum(target),
deps: deps.map do |dep| deps: deps.map do |dep|
{ {
fname: dep, fname: dep.encode(__ENCODING__),
checksum: lookup_checksum(dep), checksum: lookup_checksum(dep),
} }
end end
} }
end end
# Return a list of targets that have been built
def targets
@cache[:targets].keys
end
# Make any needed directories and record the ones that are created for
# removal upon a "clean" operation.
def mkdir_p(path)
parts = path.split(/[\\\/]/)
(0..parts.size-1).each do |i|
subpath = File.join(*parts[0, i + 1]).encode(__ENCODING__)
unless File.exists?(subpath)
FileUtils.mkdir(subpath)
@cache[:directories][subpath] = true
end
end
end
# Return a list of directories which were created as a part of the build
def directories
@cache[:directories].keys
end
# Private Instance Methods # Private Instance Methods
private private
@@ -141,7 +179,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)).encode(__ENCODING__) rescue '' @lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, mode: 'rb')).encode(__ENCODING__) rescue ''
end end
end end
end end
+89 -44
View File
@@ -6,14 +6,15 @@ 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
# [Array] of {Builder} objects. # Hash of +{"builder_name" => builder_object}+ pairs.
attr_reader :builders attr_reader :builders
# Create an Environment object. # Create an Environment object.
# @param variables [Hash] # @param variables [Hash]
# The variables hash can contain both construction variables, which are # The variables hash can contain construction variables, which are
# uppercase strings (such as "CC" or "LDFLAGS"), and RScons options, # uppercase strings (such as "CC" or "LDFLAGS"), user variables, which
# which are lowercase symbols (such as :echo). # are lowercase strings (such as "sources"), and RScons options, which
# are lowercase symbols (such as :echo).
# 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(variables = {})
@@ -21,6 +22,7 @@ module Rscons
@targets = {} @targets = {}
@builders = {} @builders = {}
@build_dirs = [] @build_dirs = []
@build_hooks = []
@varset[:exclude_builders] ||= [] @varset[:exclude_builders] ||= []
unless @varset[:exclude_builders] == :all unless @varset[:exclude_builders] == :all
exclude_builders = Set.new(@varset[:exclude_builders] || []) exclude_builders = Set.new(@varset[:exclude_builders] || [])
@@ -30,10 +32,7 @@ module Rscons
end end
end end
end end
(@varset[:builders] || []).each do |builder| @varset[:echo] ||= :short
add_builder(builder)
end
@varset[:echo] ||= :command
if block_given? if block_given?
yield self yield self
@@ -44,7 +43,7 @@ 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, builders, and build directories. It will not
# contain a copy of the targets. # contain a copy of the targets or build hooks.
# 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 clone(variables = {}) def clone(variables = {})
@@ -52,7 +51,7 @@ module Rscons
@builders.each do |builder_name, builder| @builders.each do |builder_name, builder|
env.add_builder(builder) env.add_builder(builder)
end end
env.append(@varset) env.append(@varset.clone)
env.append(variables) env.append(variables)
if block_given? if block_given?
@@ -73,29 +72,30 @@ module Rscons
end end
end end
# Add a build hook to the Environment.
def add_build_hook(&block)
@build_hooks << block
end
# Specify a build directory for this Environment. # Specify a build directory for this Environment.
# Source files from src_dir will produce object files under obj_dir. # Source files from src_dir will produce object files under obj_dir.
def build_dir(src_dir, obj_dir) def build_dir(src_dir, obj_dir)
src_dir = src_dir.gsub('\\', '/') if src_dir.is_a?(String) src_dir = src_dir.gsub('\\', '/') if src_dir.is_a?(String)
obj_dir = obj_dir.gsub('\\', '/')
@build_dirs << [src_dir, obj_dir] @build_dirs << [src_dir, obj_dir]
end end
# 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 = source_fname.set_suffix(suffix).gsub('\\', '/')
@build_dirs.each do |src_dir, obj_dir| @build_dirs.each 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!(/^#{src_dir}\//, "#{obj_dir}/") build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/")
end end
build_fname.gsub!('\\', '/') build_fname.gsub!('\\', '/')
end end
FileUtils.mkdir_p(File.dirname(build_fname))
build_fname build_fname
end end
@@ -117,41 +117,33 @@ 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)
@targets[target][:builder].run(target, process_target.call(src)
@targets[target][:source], end
cache, end
self, result = run_builder(@targets[target][:builder],
*@targets[target][:args]) target,
else @targets[target][:source],
false cache,
@targets[target][:vars] || {})
unless result
cache.write
raise BuildError.new("Failed to build #{target}")
end
result
end end
end end
@targets.each do |target, info| @targets.each do |target, info|
next if targets_processed.include?(target) process_target.call(target)
unless process_target.call(target)
$stderr.puts "Error: failed to build #{target}"
break
end
end end
cache.write cache.write
end end
@@ -172,7 +164,7 @@ 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 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(' ')
@@ -193,11 +185,15 @@ module Rscons
alias_method :orig_method_missing, :method_missing 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, *rest = args target, source, vars, *rest = args
unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet)
raise "Unexpected construction variable set: #{vars.inspect}"
end
source = [source] unless source.is_a?(Array) source = [source] unless source.is_a?(Array)
@targets[target] = { @targets[target] = {
builder: @builders[method.to_s], builder: @builders[method.to_s],
source: source, source: source,
vars: vars,
args: rest, args: rest,
} }
else else
@@ -205,11 +201,60 @@ module Rscons
end end
end end
# Build a list of source files into files containing one of the suffixes
# given by suffixes.
# This method is used internally by RScons builders.
# @param sources [Array] List of source files to build.
# @param suffixes [Array] List of suffixes to try to convert source files into.
# @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder.
# Return a list of the converted file names.
def build_sources(sources, suffixes, cache, vars = {})
sources.map do |source|
if source.has_suffix?(suffixes)
source
else
converted = nil
suffixes.each do |suffix|
converted_fname = get_build_fname(source, suffix)
builder = @builders.values.find { |b| b.produces?(converted_fname, source, self) }
if builder
converted = run_builder(builder, converted_fname, [source], cache, vars)
return nil unless converted
break
end
end
converted or raise "Could not find a builder to handle #{source.inspect}."
end
end
end
# Invoke a builder to build the given target based on the given sources.
# @param builder [Builder] The Builder to use.
# @param target [String] The target output file.
# @param sources [Array] List of source files.
# @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder.
# Return the result of the builder's run() method.
def run_builder(builder, target, sources, cache, vars)
vars = @varset.merge(vars)
@build_hooks.each do |build_hook_block|
build_operation = {
builder: builder,
target: target,
sources: sources,
vars: vars,
}
build_hook_block.call(build_operation)
end
builder.run(target, sources, cache, self, vars)
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.
# @param target [String] Name of the target to gather dependencies for. # @param target [String] Name of the target to gather dependencies for.
def parse_makefile_deps(mf_fname, target) def self.parse_makefile_deps(mf_fname, target)
deps = [] deps = []
buildup = '' buildup = ''
File.read(mf_fname).each_line do |line| File.read(mf_fname).each_line do |line|
+15 -15
View File
@@ -8,7 +8,11 @@ module Rscons
# Create a VarSet # Create a VarSet
# @param vars [Hash] Optional initial variables. # @param vars [Hash] Optional initial variables.
def initialize(vars = {}) def initialize(vars = {})
@vars = vars if vars.is_a?(VarSet)
@vars = vars.clone.vars
else
@vars = vars
end
end end
# Access the value of variable as a particular type # Access the value of variable as a particular type
@@ -20,9 +24,9 @@ module Rscons
# returned. # returned.
def [](key, type = nil) def [](key, type = nil)
val = @vars[key] val = @vars[key]
if type == :array and val.is_a?(String) if type == :array and not val.is_a?(Array)
[val] [val]
elsif type == :string and val.is_a?(Array) elsif type == :scalar and val.is_a?(Array)
val.first val.first
else else
val val
@@ -40,7 +44,7 @@ module Rscons
# @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) values = values.vars if values.is_a?(VarSet)
@vars.merge!(values) @vars.merge!(Marshal.load(Marshal.dump(values)))
self self
end end
@@ -60,20 +64,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}"
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.2" VERSION = "0.1.0"
end end
+204 -29
View File
@@ -1,27 +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
def build_testdir
if File.exists?("build.rb")
system("ruby -I #{@owd}/lib -r rscons build.rb > build.out")
end
get_build_output
end end
def test_dir(build_test_directory) def test_dir(build_test_directory)
FileUtils.cp_r("build_tests/#{build_test_directory}", 'build_tests_run') FileUtils.cp_r("build_tests/#{build_test_directory}", BUILD_TEST_RUN_DIR)
Dir.chdir("build_tests_run") Dir.chdir(BUILD_TEST_RUN_DIR)
build_testdir
end end
def file_sub(fname) def file_sub(fname)
@@ -35,8 +39,10 @@ describe Rscons do
end end
end end
def get_build_output def lines
File.read('build.out').lines.map(&:strip) @output.lines.map(&:rstrip).tap do |v|
@output = ""
end
end end
########################################################################### ###########################################################################
@@ -45,12 +51,19 @@ describe Rscons do
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',
@@ -58,7 +71,10 @@ describe Rscons do
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',
@@ -67,68 +83,157 @@ describe Rscons do
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
it 'cleans built files' do
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"
File.exists?('build_one/one.o').should be_true
File.exists?('build_two/two.o').should be_true
Rscons.clean
File.exists?('build_one/one.o').should be_false
File.exists?('build_two/two.o').should be_false
File.exists?('build_one').should be_false
File.exists?('build_two').should be_false
File.exists?('src/one/one.c').should be_true
end
it 'does not clean created directories if other non-rscons-generated files reside there' do
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"
File.exists?('build_one/one.o').should be_true
File.exists?('build_two/two.o').should be_true
File.open('build_two/tmp', 'w') { |fh| fh.puts "dum" }
Rscons.clean
File.exists?('build_one/one.o').should be_false
File.exists?('build_two/two.o').should be_false
File.exists?('build_one').should be_false
File.exists?('build_two').should be_true
File.exists?('src/one/one.c').should be_true
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 '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('CPPFLAGS' => '-DSTRING="Release Version"') do |env|
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},
@@ -139,8 +244,78 @@ describe Rscons do
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
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 == [
'gcc -c -o one.o -MMD -MF one.mf -DONE one.c',
'gcc -c -o two.o -MMD -MF two.mf two.c',
'gcc -o two_sources one.o two.o',
]
File.exists?('two_sources').should be_true
`./two_sources`.should == "This is a C program with two sources.\n"
end
it 'builds a static library archive' do
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 == [
'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',
'ar rcs lib.a one.o two.o',
'gcc -c -o three.o -MMD -MF three.mf three.c',
'gcc -o library lib.a three.o',
]
File.exists?('library').should be_true
`ar t lib.a`.should == "one.o\ntwo.o\n"
end
it 'supports build hooks to override construction variables' do
test_dir("build_dir")
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 =~ [
'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 -o build_hook build_one/one.o build_two/two.o',
]
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
+204
View File
@@ -0,0 +1,204 @@
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
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", []).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", []).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", []).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", []).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"]).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"}]}}}
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"]).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"}]}}}
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"], strict_deps: true).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"}]}}}
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"]).should be_true
end
end
describe "#register_build" do
it "stores the given information in the cache" do
_cache = {}
cache = build_from(_cache)
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.register_build("the target", "the command", ["dep 1", "dep 2"])
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"},
]
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
+267 -5
View File
@@ -1,12 +1,275 @@
module Rscons module Rscons
describe Environment do describe Environment do
describe '.parse_makefile_deps' do describe "#initialize" do
it "stores the construction variables passed in" do
env = Environment.new("CFLAGS" => ["-g"], "CPPPATH" => ["dir"])
env["CFLAGS"].should == ["-g"]
env["CPPPATH"].should == ["dir"]
end
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: :all)
env.builders.size.should == 0
end
it "excludes the named builders" do
env = Environment.new(exclude_builders: ["Library"])
env.builders.size.should be > 0
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 be_nil
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
env = Environment.new
env["CPPPATH"] << "path1"
env2 = env.clone
env2["CPPPATH"] << "path2"
env["CPPPATH"].should == ["path1"]
env2["CPPPATH"].should == ["path1", "path2"]
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
describe "#add_builder" do
it "adds the builder to the list of builders" do
env = Environment.new(exclude_builders: :all)
env.builders.keys.should == []
env.add_builder(Rscons::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
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
end
end
describe "#[]" do
it "allows reading construction variables" do
env = Environment.new("CFLAGS" => ["-g", "-Wall"])
env["CFLAGS"].should == ["-g", "-Wall"]
end
end
describe "#[]=" do
it "allows writing construction variables" do
env = Environment.new("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("CFLAGS" => ["-g"], "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 "#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 "#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 "#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
@@ -15,8 +278,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
@@ -0,0 +1,6 @@
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
+119
View File
@@ -0,0 +1,119 @@
module Rscons
describe VarSet do
describe '.initialize' do
it "initializes variables from a Hash" do
v = VarSet.new({"one" => 1, "two" => :two})
v["one"].should == 1
v["two"].should == :two
end
it "initializes variables from another VarSet" do
v = VarSet.new({"one" => 1})
v2 = VarSet.new(v)
v2["one"].should == 1
end
it "makes a deep copy of the given VarSet" do
v = VarSet.new({"array" => [1, 2, 3]})
v2 = VarSet.new(v)
v["array"] << 4
v["array"].should == [1, 2, 3, 4]
v2["array"].should == [1, 2, 3]
end
end
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
v["fuz"].should == "a string"
v["foo"].should == 42
v["bar"].should == :baz
v["qax"].should == [3, 6]
v["qux"].should == {a: :b}
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
describe :[]= do
it "allows assigning to variables" do
v = VarSet.new("CFLAGS" => ["-Wall", "-O3"])
v["CPPPATH"] = ["one", "two"]
v["CFLAGS"].should == ["-Wall", "-O3"]
v["CPPPATH"].should == ["one", "two"]
end
end
describe '.append' do
it "adds values from a Hash to the VarSet" do
v = VarSet.new("LDFLAGS" => "-lgcc")
v.append("LIBS" => "gcc", "LIBPATH" => ["mylibs"])
v.vars.keys.should =~ ["LDFLAGS", "LIBS", "LIBPATH"]
end
it "adds values from another VarSet to the VarSet" do
v = VarSet.new("CPPPATH" => ["mydir"])
v2 = VarSet.new("CFLAGS" => ["-O0"], "CPPPATH" => ["different_dir"])
v.append(v2)
v.vars.keys.should =~ ["CPPPATH", "CFLAGS"]
v["CPPPATH"].should == ["different_dir"]
end
end
describe '.merge' do
it "returns a new VarSet merged with the given Hash" do
v = VarSet.new("foo" => "yoda")
v2 = v.merge("baz" => "qux")
v.vars.keys.should == ["foo"]
v2.vars.keys.should =~ ["foo", "baz"]
end
it "returns a new VarSet merged with the given VarSet" do
v = VarSet.new("foo" => ["a", "b"], "bar" => 42)
v2 = v.merge(VarSet.new("bar" => 33, "baz" => :baz))
v2["foo"] << "c"
v["foo"].should == ["a", "b"]
v["bar"].should == 42
v2["foo"].should == ["a", "b", "c"]
v2["bar"].should == 33
end
end
describe '.expand_varref' do
v = VarSet.new("CFLAGS" => ["-Wall", "-O2"],
"CC" => "gcc",
"CPPPATH" => ["dir1", "dir2"],
"compiler" => "${CC}",
"cmd" => ["${CC}", "-c", "${CFLAGS}", "-I${CPPPATH}"])
it "expands to the string itself if the string is not a variable reference" do
v.expand_varref("CC").should == "CC"
v.expand_varref("CPPPATH").should == "CPPPATH"
v.expand_varref("str").should == "str"
end
it "expands a single variable reference beginning with a '$'" do
v.expand_varref("${CC}").should == "gcc"
v.expand_varref("${CPPPATH}").should == ["dir1", "dir2"]
end
it "expands a single variable reference in ${arr} notation" do
v.expand_varref("prefix${CFLAGS}suffix").should == ["prefix-Wallsuffix", "prefix-O2suffix"]
v.expand_varref(v["cmd"]).should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"]
end
it "expands a variable reference recursively" do
v.expand_varref("${compiler}").should == "gcc"
v.expand_varref("${cmd}").should == ["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"]
end
it "resolves multiple variable references in one element by enumerating all combinations" do
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
it "raises an error when a variable reference refers to a non-existent variable" do
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
+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
+6
View File
@@ -1 +1,7 @@
require "simplecov"
SimpleCov.start do
add_filter "/spec/"
end
require "rscons" require "rscons"