Compare commits

...
161 Commits
Author SHA1 Message Date
josh 01a84e8819 v1.6.0 2014-06-18 10:45:05 -04:00
josh 28bedfce11 Support lambdas as construction variable values 2014-06-18 10:43:35 -04:00
josh e792fd473b add v1.5.0 release notes 2014-06-18 09:15:49 -04:00
josh 9f738ee175 v1.5.0 2014-06-16 22:20:06 -04:00
josh 912615535a added more documentation to get to 100% YARD coverage; fix YARD warnings 2014-06-16 15:33:22 -04:00
josh d87c4990b1 add redcarpet to bundle 2014-06-16 14:40:43 -04:00
josh 6a17cbcd54 remove unnecessary rspec dependencies 2014-06-16 14:37:24 -04:00
josh 8fc95b5d69 change Builder#create_build_target and BuildTarget#initialize to accept a Hash of options 2014-06-16 12:17:33 -04:00
josh d9eea10363 update D build test for Linux 2014-06-12 16:00:41 -04:00
josh 5045e2e336 fix build test for Linux 2014-06-12 15:59:49 -04:00
josh 31968e7e32 complete spec updates for RSpec 3 using only :expect syntax 2014-06-12 15:52:15 -04:00
josh 5783ed993a update more rspec expectations 2014-06-12 15:43:50 -04:00
josh 0778f043ee update many specs to RSpec 3 format 2014-06-12 15:39:20 -04:00
josh 42597b2dac use new rspec expect syntax 2014-06-12 13:54:35 -04:00
josh 7b13d51d23 environment spec updates for Windows 2014-06-12 13:41:46 -04:00
josh c2d4ece957 build test spec fix for running on Windows 2014-06-12 13:38:15 -04:00
josh 3ab9c8723b fix a build test for MinGW 2014-06-12 13:35:11 -04:00
josh 1b50a9dd3c add Rscons::BuildTarget
Return a BuildTarget object from Environment#method_missing when a build target is registered.
2014-06-12 13:31:17 -04:00
josh 87b25d7bfd add PROGSUFFIX construction variable and default it to ".exe" for mingw/cygwin platforms 2014-06-12 13:02:53 -04:00
josh da457b9138 fix spec typo 2014-06-12 11:31:56 -04:00
josh 0cbf05da49 retry directory removal to work around Windows' inability to remove directories when you ask 2014-06-12 11:31:17 -04:00
josh 08b8a2e6be update gemspec, do not use git to determine files 2014-06-12 11:13:56 -04:00
josh 9f75ea9d21 add Rscons.command_executer to determine command executer
Use "env" to execute commands when in MSYS on Windows
2014-06-12 11:07:09 -04:00
josh ef18c9da35 Environment: add #parse_flags, #parse_flags!, and #merge_flags 2014-06-12 10:40:01 -04:00
josh 7c9e7e833a Unbuffer $stdout 2014-06-12 09:36:24 -04:00
josh eb9aa476b7 update default construction variables to match SCons a little more closely 2014-06-11 16:54:32 -04:00
josh 7f2b685bfb shorten Environment#shell code a bit 2014-06-11 15:31:13 -04:00
josh ae9021a505 add Environment#shell - close #11 2014-06-11 14:59:31 -04:00
josh a1a4cc18cd change json to a runtime gem dependency 2014-06-05 09:19:59 -04:00
josh f5a3b448d3 test supported construction variable types when cloning 2014-05-15 15:54:02 -04:00
josh 9abb47ffe7 v1.4.3 2014-05-14 15:53:52 -04:00
josh f78d13b648 add v1.4.3 release notes 2014-05-14 15:53:33 -04:00
josh ab1ea34941 add specs testing construction variable overrides for built-in builders 2014-05-14 15:48:00 -04:00
josh 08f2248ed7 fix up builders to properly use variable overrides 2014-05-14 15:15:08 -04:00
josh 9d61fa0276 merge Environment#build_command and #expand_varref 2014-05-14 14:33:09 -04:00
josh c0adb64c5d v1.4.2 2014-05-09 10:32:22 -04:00
josh 95da14e029 add release notes in README.md 2014-05-09 10:31:54 -04:00
josh 58b41df426 expand target and sources for construction variable references before invoking builder 2014-05-09 09:45:29 -04:00
josh bd525e2742 rename Environment#clean_target_paths! -> #expand_paths! 2014-05-09 09:19:40 -04:00
josh c08a2243b9 add Environment#expand_path 2014-05-05 16:22:07 -04:00
josh 0de1e8e970 v1.4.1 2014-04-17 12:03:29 -04:00
josh 6f20a1da3f bugfix: support invoking builders with no sources and a build root defined 2014-04-17 12:03:09 -04:00
josh 03ffb39956 v1.4.0 2014-04-15 16:19:19 -04:00
josh 0a10af3f5e update README.md for v1.4.0 release notes 2014-04-15 16:18:58 -04:00
josh ddf00d08a7 shortcut all of Environment#process if @targets is empty 2014-04-15 16:18:33 -04:00
josh 8be1842cf8 update documentation for new builders 2014-04-15 16:06:00 -04:00
josh d424facf7f Add Preprocess and Disassemble builders 2014-04-15 15:21:55 -04:00
josh ee5aca275d Write Cache to disk if an exception occurs while processing the Environment - fix #4 2014-04-15 14:20:30 -04:00
josh ea30b0b5ee Fix Cache#mkdir_p to handle absolute paths - fix #5 2014-04-15 14:15:32 -04:00
josh 5a0d80b781 Cache: implement "dirty" attribute to only re-serialize when necessary 2014-04-15 14:11:10 -04:00
josh a2653a77d1 shortcut Environment#process if @targets is empty 2014-04-15 14:04:59 -04:00
josh 160434b903 Cache: change to a Singleton object so it is only loaded from disk once 2014-04-15 14:02:26 -04:00
josh 5ce8940d54 stop encoding Cache contents 2014-04-15 13:39:45 -04:00
josh 1a5e9ddaa6 Implement copy-on-write semantics for VarSet, speeding up Environment cloning 2014-04-15 11:21:15 -04:00
josh 5924154229 speed up VarSet#deep_dup a bit 2014-04-15 09:47:05 -04:00
josh 5beda54374 do not clone @varset when appending to the cloned environment since appending will do its own clone 2014-04-15 09:36:30 -04:00
josh ae0ad98075 use @varset.append() instead of @varset.send(:append) 2014-04-15 09:32:35 -04:00
josh f5be3a3571 VarSet: implement deep copying ourselves instead of using Marshal for a speed increase 2014-04-15 09:16:30 -04:00
josh fdc5c8773c Issue #7 - store MD5 of build command instead of the full command in the cache 2014-04-14 17:17:54 -04:00
josh c61380b354 Issue #7 - change .rsconscache from using YAML to JSON to speed up parsing 2014-04-14 16:52:21 -04:00
josh d8dda291a5 ensure that Dir[] results are predictable in build tests 2014-04-14 13:13:43 -04:00
josh 1fe2c6401d expand ^/ in target paths to be relative to the build root 2014-03-27 13:30:11 -04:00
josh 4818fd10fe README.md: add more details about Environment cloning 2014-03-27 12:02:46 -04:00
josh 5989e24ee6 update some documentation 2014-03-27 11:57:45 -04:00
josh 1875a2ea66 Pass the Environment object to build hooks in the :env key of the build_op parameter 2014-03-27 10:44:10 -04:00
josh cbd5816b4b add VarSet#include? 2014-03-27 10:39:46 -04:00
josh b2c970c9ca add Environment#add_target and use it from Environment#method_missing 2014-03-26 11:29:02 -04:00
josh 92a9982988 use #super from #method_missing instead of method aliasing 2014-03-26 11:23:32 -04:00
josh 533ee9c16c add CFile builder to create C/C++ sources from lex/yacc input files 2014-03-18 21:57:53 -04:00
josh 831b9bf2c7 v1.3.0 2014-02-25 12:18:02 -05:00
josh 031395bb57 add v1.3.0 release notes 2014-02-25 12:17:49 -05:00
josh e3aeb7f4d7 Change Environment#execute() options Hash to contain :env and :options keys for Kernel#system() 2014-02-25 12:14:52 -05:00
josh 6aa5037234 v1.2.0 2014-02-20 16:42:10 -05:00
josh 0d8f661fd4 add :clone option to Environment#clone to control exactly which Environment attributes are cloned 2014-02-19 15:02:46 -05:00
josh e5ca8776a9 allow nil to be passed in to Environment#build_root= 2014-02-19 15:02:18 -05:00
josh 2dde39a0ec standardize on "Rscons" capitalization 2014-02-18 16:14:44 -05:00
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
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
40 changed files with 3233 additions and 544 deletions

No files matched your search

+1 -1
View File
@@ -15,4 +15,4 @@ spec/reports
test/tmp
test/version_tmp
tmp
build_tests_run
build_test_run
+368 -13
View File
@@ -2,15 +2,17 @@
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
Add this line to your application's Gemfile:
gem 'rscons'
gem "rscons"
And then execute:
$ bundle
$ bundle install
Or install it yourself as:
@@ -18,20 +20,373 @@ Or install it yourself as:
## Usage
RScons::Environment.new do |env|
env.Program('program', Dir['*.c'])
end
Rscons is a Ruby library.
It can be called from a standalone Ruby script or it can be used with rake and
called from your Rakefile.
main_env = RScons::Environment.new do |env|
env.build_dir('src', 'build/main')
env.Program('program', Dir['src/**/*.cc'])
end
### Example: Building a C Program
debug_env = main_env.clone do |env|
env.build_dir('src', 'build/debug')
env['CFLAGS'] = ['-g', '-O0']
env.Program('program-debug', Dir['src/**/*.cc'])
```ruby
Rscons::Environment.new do |env|
env["CFLAGS"] << "-Wall"
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
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
* `:env` - `Environment` calling the build hook; note that this may be
different from the Environment that the build hook was added to in the case
that the original Environment was cloned with build hooks!
* `:target` - `String` name of the target file
* `: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
```
### Example: Creating a C++ parser source from a Yacc/Bison input file
```ruby
Rscons::Environment.new do |env|
env.CFile("#{env.build_root}/parser.tab.cc", "parser.yy")
end
```
## Details
### Builders
Rscons ships with a number of builders:
* CFile, which builds a C or C++ source file from a lex or yacc input file
* Disassemble, which disassembles an object file to a disassembly listing
* Library, which collects object files into a static library archive file
* Object, which compiles source files to produce an object file
* Preprocess, which invokes the C/C++ preprocessor on a source file
* Program, which links object files to produce an executable
If you want to create an Environment that does not contain any builders,
you can use the `exclude_builders` key to the Environment constructor.
#### CFile
```ruby
env.CFile(target, source)
# Example
env.CFile("parser.c", "parser.y")
```
The CFile builder will generate a C or C++ source file from a lex (.l, .ll)
or yacc (.y, .yy) input file.
#### Disassemble
```ruby
env.Disassemble(target, source)
# Example
env.Disassemble("module.dis", "module.o")
```
The Disassemble builder generates a disassembly listing using objdump from
and object file.
#### Library
```ruby
env.Library(target, sources)
# Example
env.Library("lib.a", Dir["src/**/*.c"])
```
The Library builder creates a static library archive from the given source
files.
#### Object
```ruby
env.Object(target, sources)
# Example
env.Object("module.o", "module.c")
```
The Object builder compiles the given sources to an object file.
#### Preprocess
```ruby
env.Preprocess(target, source)
# Example
env.Preprocess("module-preprocessed.cc", "module.cc")
```
The Preprocess builder invokes either `${CC}` or `${CXX}` (depending on if the
source contains an extension in `${CXXSUFFIX}` or not) and writes the
preprocessed output to the target file.
#### Program
```ruby
env.Program(target, sources)
# Example
env.Program("myprog", Dir["src/**/*.cc"])
```
The Program builder compiles and links the given sources to an executable file.
Object files or source files can be given as `sources`.
### Managing Environments
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, by default 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.
The set of environment attributes that are cloned is controllable via the
`:clone` option to the `#clone` method.
For example, `env.clone(clone: :all)` will include construction variables,
builders, build hooks, build directories, and the build root.
The set of pending targets is never cloned.
Cloned environments contain "deep copies" of construction variables.
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
* symbols, 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.5.0
- add "json" as a runtime dependency
- update construction variables to match SCons more closely
- add CPPDEFPREFIX, INCPREFIX, CPPDEFINES, CCFLAGS, LIBDIRPREFIX, and LIBLINKPREFIX
- add Environment#shell
- add Environment#parse_flags, #parse_flags!, #merge_flags
- unbuffer $stdout by default
- add PROGSUFFIX construction variable (defaults to .exe on MinGW/Cygwin)
- add Rscons::BuildTarget and Builder#create_build_target
- update specs to RSpec 3.x and fix to run on MinGW/Cygwin/Linux
- add YARD documentation to get to 100% coverage
### v1.4.3
- fix builders properly using construction variable overrides
- expand nil construction variables to empty strings
### v1.4.2
- add Environment#expand_path
- expand construction variable references in builder targets and sources before invoking builder
### v1.4.1
- fix invoking a builder with no sources while a build root defined
### v1.4.0
- add CFile builder
- add Disassemble builder
- add Preprocess builder
- pass the Environment object to build hooks in the :env key of the build_op parameter
- expand target/source paths beginning with "^/" to be relative to the Environment's build root
- many performance improvements, including:
- use JSON instead of YAML for the cache to improve loading speed (Issue #7)
- store a hash of the build command instead of the full command contents in the cache
- implement copy-on-write semantics for construction variables when cloning Environments
- only load the cache once instead of on each Environment#process
- only write the cache when something has changed
- fix Cache#mkdir_p to handle relative paths (Issue #5)
- flush the cache to disk if a builder raises an exception (Issue #4)
### v1.3.0
- change Environment#execute() options parameter to accept the following options keys:
- :env to pass an environment Hash to Kernel#system
- :options to pass an options Hash to Kernel#system
### v1.2.0
- add :clone option to Environment#clone to control exactly which Environment attributes are cloned
- allow nil to be passed in to Environment#build_root=
### 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
+8 -3
View File
@@ -7,11 +7,8 @@ end
require "bundler/gem_tasks"
require "rspec/core/rake_task"
require "rake/clean"
require "yard"
CLEAN.include 'build_tests_run'
RSpec::Core::RakeTask.new(:spec)
YARD::Rake::YardocTask.new do |yard|
@@ -19,3 +16,11 @@ YARD::Rake::YardocTask.new do |yard|
end
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, vars = {})
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
-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
+105 -19
View File
@@ -1,41 +1,127 @@
require "rscons/builder"
require "rscons/cache"
require "rscons/environment"
require "rscons/varset"
require "rscons/version"
require "rscons/monkey/module"
require "rscons/monkey/string"
require_relative "rscons/build_target"
require_relative "rscons/builder"
require_relative "rscons/cache"
require_relative "rscons/environment"
require_relative "rscons/varset"
require_relative "rscons/version"
# default builders
require "rscons/builders/library"
require "rscons/builders/object"
require "rscons/builders/program"
require_relative "rscons/builders/cfile"
require_relative "rscons/builders/disassemble"
require_relative "rscons/builders/library"
require_relative "rscons/builders/object"
require_relative "rscons/builders/preprocess"
require_relative "rscons/builders/program"
# Namespace module for rscons classes
module Rscons
# Names of the default builders which will be added to all newly created
# {Environment} objects.
DEFAULT_BUILDERS = [
Library,
Object,
Program,
:CFile,
:Disassemble,
:Library,
:Object,
:Preprocess,
:Program,
]
class BuildError < Exception
end
# Class to represent a fatal error while building a target.
class BuildError < RuntimeError; end
# Remove all generated files
# Remove all generated files.
def self.clean
cache = Cache.new
cache = Cache.instance
# 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
cache.clear
end
# Return whether the given path is an absolute filesystem path.
#
# @param path [String] the path to examine.
#
# @return [Boolean] Whether the given path is an absolute filesystem path.
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, e.g. ".exe".
#
# @return [String] New path.
def self.set_suffix(path, suffix)
path.sub(/\.[^.]*$/, suffix)
end
# Return the system shell and arguments for executing a shell command.
#
# @return [Array<String>] The shell and flag.
def self.get_system_shell
@@shell ||=
begin
test_shell = lambda do |*args|
begin
"success" == IO.popen([*args, "echo success"]) do |io|
io.read.strip
end
rescue
false
end
end
if ENV["SHELL"] and ENV["SHELL"] != "" and test_shell[ENV["SHELL"], "-c"]
[ENV["SHELL"], "-c"]
elsif Object.const_get("RUBY_PLATFORM") =~ /mingw/
if test_shell["sh", "-c"]
# Using Rscons from MSYS should use MSYS's shell.
["sh", "-c"]
else
["cmd", "/c"]
end
else
["sh", "-c"]
end
end
end
# Return an Array containing a command used to execute commands.
#
# This will normally be an empty Array, but on Windows if Rscons detects
# that it is running in MSYS then ["env"] will be returned.
#
# @return [Array<String>] Command used to execute commands.
def self.command_executer
@@command_executer ||=
if Object.const_get("RUBY_PLATFORM") =~ /mingw/
if ENV.keys.find {|key| key =~ /MSYS/}
begin
if IO.popen(["env", "echo", "success"]) {|io| io.read.strip} == "success"
["env"]
end
rescue
end
end
end || []
end
# Set the command executer array.
#
# @param val [Array<String>] Command used to execute commands.
def self.command_executer=(val)
@@command_executer = val
end
end
# Unbuffer $stdout
$stdout.sync = true
+36
View File
@@ -0,0 +1,36 @@
module Rscons
# The BuildTarget class represents a single build target.
class BuildTarget
# Create a BuildTarget object.
#
# @param options [Hash] Options to create the BuildTarget with.
# @option options [Environment] :env
# The Environment.
# @option options [String] :target
# The user-supplied target name.
# @option options [Array<String>] :sources
# The user-supplied source file name(s).
def initialize(options)
@env = options[:env]
@target = options[:target]
end
# Manually record a given target as depending on the specified files.
#
# @param user_deps [Array<String>] Dependency files.
#
# @return [void]
def depends(*user_deps)
@env.depends(@target, *user_deps)
end
# Convert the BuildTarget to a String.
#
# This method always returns the target file name.
#
# @return [String] Target file name.
def to_s
@target
end
end
end
+52 -5
View File
@@ -1,33 +1,80 @@
require "fileutils"
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 Builder
# Return a set of default variable values for the Environment to use
# unless the user overrides any.
# 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 construction variables for the builder.
#
# @param env [Environment] The Environment.
#
# @return [Hash] Default construction variables.
def default_variables(env)
{}
end
# Create a BuildTarget object for this build target.
#
# Builder sub-classes can override this method to manipulate parameters
# (for example, add a suffix to the user-given target file name).
#
# @param options [Hash] Options to create the BuildTarget with.
# @option options [Environment] :env
# The Environment.
# @option options [String] :target
# The user-supplied target name.
# @option options [Array<String>] :sources
# The user-supplied source file name(s).
#
# @return [BuildTarget]
def create_build_target(options)
BuildTarget.new(options)
end
# Return whether this builder object is capable of producing a given target
# file name from a given source file name.
#
# @param target [String] The target file name.
# @param source [String, Array] The source file name(s).
# @param env [Environment] The Environment.
#
# @return [Boolean]
# Whether this builder object is capable of producing a given target
# file name from a given source file name.
def produces?(target, source, env)
false
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.
#
# @param short_cmd_string [String]
# Short description of build action to be printed when env.echo ==
# :short.
# @param target [String] Name of the target file.
# @param command [Array<String>]
# The command to execute to build the target.
# @param sources [Array<String>] Source file name(s).
# @param env [Environment] The Environment executing the builder.
# @param cache [Cache] The Cache object.
#
# @return [String,false]
# The name of the target on success or false on failure.
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))
FileUtils.rm_f(target)
return false unless env.execute(short_cmd_string, command)
cache.register_build(target, command, sources)
cache.register_build(target, command, sources, env)
end
target
end
+55
View File
@@ -0,0 +1,55 @@
module Rscons
module Builders
# Build a C or C++ source file given a lex (.l, .ll) or yacc (.y, .yy)
# input file.
#
# Examples::
# env.CFile("parser.tab.cc", "parser.yy")
# env.CFile("lex.yy.cc", "parser.ll")
class CFile < Builder
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
"YACC" => "bison",
"YACC_FLAGS" => ["-d"],
"YACC_CMD" => ["${YACC}", "${YACC_FLAGS}", "-o", "${_TARGET}", "${_SOURCES}"],
"LEX" => "flex",
"LEX_FLAGS" => [],
"LEX_CMD" => ["${LEX}", "${LEX_FLAGS}", "-o", "${_TARGET}", "${_SOURCES}"],
}
end
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars)
vars = vars.merge({
"_TARGET" => target,
"_SOURCES" => sources,
})
cmd =
case
when sources.first.end_with?(".l", ".ll")
"LEX"
when sources.first.end_with?(".y", ".yy")
"YACC"
else
raise "Unknown source file #{sources.first.inspect} for CFile builder"
end
command = env.build_command("${#{cmd}_CMD}", vars)
standard_build("#{cmd} #{target}", target, command, sources, env, cache)
end
end
end
end
+40
View File
@@ -0,0 +1,40 @@
module Rscons
module Builders
# The Disassemble builder produces a disassembly listing of a source file.
class Disassemble < Builder
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
"OBJDUMP" => "objdump",
"DISASM_CMD" => ["${OBJDUMP}", "${DISASM_FLAGS}", "${_SOURCES}"],
"DISASM_FLAGS" => ["--disassemble", "--source"],
}
end
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars)
vars = vars.merge("_SOURCES" => sources)
command = env.build_command("${DISASM_CMD}", vars)
unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target))
return false unless env.execute("Disassemble #{target}", command, options: {out: target})
cache.register_build(target, command, sources, env)
end
target
end
end
end
end
+23 -8
View File
@@ -1,28 +1,43 @@
require 'fileutils'
module Rscons
# A default RScons builder that produces a static library archive.
module Builders
# A default Rscons builder that produces a static library archive.
class Library < Builder
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
'AR' => 'ar',
'LIBSUFFIX' => '.a',
'ARFLAGS' => [],
'ARCOM' => ['$AR', 'rcs', '$ARFLAGS', '$_TARGET', '$_SOURCES']
'ARFLAGS' => ['rcs'],
'ARCMD' => ['${AR}', '${ARFLAGS}', '${_TARGET}', '${_SOURCES}']
}
end
def run(target, sources, cache, env, vars = {})
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars)
# build sources to linkable objects
objects = env.build_sources(sources, [env['OBJSUFFIX'], env['LIBSUFFIX']].flatten, cache, vars)
objects = env.build_sources(sources, env.expand_varref(["${OBJSUFFIX}", "${LIBSUFFIX}"], vars).flatten, cache, vars)
if objects
vars = vars.merge({
'_TARGET' => target,
'_SOURCES' => objects,
})
command = env.build_command(env['ARCOM'], vars)
command = env.build_command("${ARCMD}", vars)
standard_build("AR #{target}", target, command, objects, env, cache)
end
end
end
end
end
+72 -30
View File
@@ -1,71 +1,113 @@
module Rscons
# A default RScons builder which knows how to produce an object file from
module Builders
# A default Rscons builder which knows how to produce an object file from
# various types of source files.
class Object < Builder
# Mapping of known sources from which to build object files.
KNOWN_SUFFIXES = {
"AS" => "ASSUFFIX",
"CC" => "CSUFFIX",
"CXX" => "CXXSUFFIX",
"DC" => "DSUFFIX",
}
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
'OBJSUFFIX' => '.o',
'AS' => '$CC',
'CPPDEFPREFIX' => '-D',
'INCPREFIX' => '-I',
'AS' => '${CC}',
'ASFLAGS' => [],
'ASSUFFIX' => '.S',
'ASPPPATH' => '$CPPPATH',
'ASPPFLAGS' => '$CPPFLAGS',
'ASDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'],
'ASCOM' => ['$AS', '-c', '-o', '$_TARGET', '$ASDEPGEN', '-I$[ASPPPATH]', '$ASPPFLAGS', '$ASFLAGS', '$_SOURCES'],
'ASPPPATH' => '${CPPPATH}',
'ASPPFLAGS' => '${CPPFLAGS}',
'ASDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'ASCMD' => ['${AS}', '-c', '-o', '${_TARGET}', '${ASDEPGEN}', '${INCPREFIX}${ASPPPATH}', '${ASPPFLAGS}', '${ASFLAGS}', '${_SOURCES}'],
'CPPFLAGS' => [],
'CPPFLAGS' => ['${CPPDEFPREFIX}${CPPDEFINES}'],
'CPPDEFINES' => [],
'CPPPATH' => [],
'CCFLAGS' => [],
'CC' => 'gcc',
'CFLAGS' => [],
'CSUFFIX' => '.c',
'CCDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'],
'CCCOM' => ['$CC', '-c', '-o', '$_TARGET', '$CCDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CFLAGS', '$_SOURCES'],
'CCDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'CCCMD' => ['${CC}', '-c', '-o', '${_TARGET}', '${CCDEPGEN}', '${INCPREFIX}${CPPPATH}', '${CPPFLAGS}', '${CFLAGS}', '${CCFLAGS}', '${_SOURCES}'],
'CXX' => 'g++',
'CXXFLAGS' => [],
'CXXSUFFIX' => '.cc',
'CXXDEPGEN' => ['-MMD', '-MF', '$_DEPFILE'],
'CXXCOM' =>['$CXX', '-c', '-o', '$_TARGET', '$CXXDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CXXFLAGS', '$_SOURCES'],
'CXXDEPGEN' => ['-MMD', '-MF', '${_DEPFILE}'],
'CXXCMD' =>['${CXX}', '-c', '-o', '${_TARGET}', '${CXXDEPGEN}', '${INCPREFIX}${CPPPATH}', '${CPPFLAGS}', '${CXXFLAGS}', '${CCFLAGS}', '${_SOURCES}'],
'DC' => 'gdc',
'DFLAGS' => [],
'DSUFFIX' => '.d',
'D_IMPORT_PATH' => [],
'DCCMD' => ['${DC}', '-c', '-o', '${_TARGET}', '${INCPREFIX}${D_IMPORT_PATH}', '${DFLAGS}', '${_SOURCES}'],
}
end
# Return whether this builder object is capable of producing a given target
# file name from a given source file name.
#
# @param target [String] The target file name.
# @param source [String, Array] The source file name(s).
# @param env [Environment] The Environment.
#
# @return [Boolean]
# Whether this builder object is capable of producing a given target
# file name from a given source file name.
def produces?(target, source, env)
target.has_suffix?(env['OBJSUFFIX']) and (
source.has_suffix?(env['ASSUFFIX']) or
source.has_suffix?(env['CSUFFIX']) or
source.has_suffix?(env['CXXSUFFIX']))
target.end_with?(*env['OBJSUFFIX']) and KNOWN_SUFFIXES.find do |compiler, suffix_var|
source.end_with?(*env[suffix_var])
end
end
def run(target, sources, cache, env, vars = {})
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars)
vars = vars.merge({
'_TARGET' => target,
'_SOURCES' => sources,
'_DEPFILE' => target.set_suffix('.mf'),
'_DEPFILE' => Rscons.set_suffix(target, '.mf'),
})
com_prefix = if sources.first.has_suffix?(env['ASSUFFIX'])
'AS'
elsif sources.first.has_suffix?(env['CSUFFIX'])
'CC'
elsif sources.first.has_suffix?(env['CXXSUFFIX'])
'CXX'
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)
com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var|
sources.first.end_with?(*env.expand_varref("${#{suffix_var}}"))
end.tap do |v|
v.nil? and raise "Error: unknown input file type: #{sources.first.inspect}"
end.first
command = env.build_command("${#{com_prefix}CMD}", vars)
unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target))
FileUtils.rm_f(target)
return false unless env.execute("#{com_prefix} #{target}", command)
deps = sources
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'])
end
cache.register_build(target, command, deps.uniq)
cache.register_build(target, command, deps.uniq, env)
end
target
end
end
end
end
+40
View File
@@ -0,0 +1,40 @@
module Rscons
module Builders
# The Preprocess builder invokes the C preprocessor
class Preprocess < Builder
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
"CPP_CMD" => ["${_PREPROCESS_CC}", "-E", "-o", "${_TARGET}", "-I${CPPPATH}", "${CPPFLAGS}", "${CFLAGS}", "${_SOURCES}"],
}
end
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars)
pp_cc = if sources.find {|s| s.end_with?(*env.expand_varref("${CXXSUFFIX}", vars))}
"${CXX}"
else
"${CC}"
end
vars = vars.merge("_PREPROCESS_CC" => pp_cc,
"_TARGET" => target,
"_SOURCES" => sources)
command = env.build_command("${CPP_CMD}", vars)
standard_build("Preprocess #{target}", target, command, sources, env, cache)
end
end
end
end
+59 -12
View File
@@ -1,33 +1,80 @@
module Rscons
# A default RScons builder that knows how to link object files into an
module Builders
# A default Rscons builder that knows how to link object files into an
# executable program.
class Program < Builder
# Return default construction variables for the builder.
#
# @param env [Environment] The Environment using the builder.
#
# @return [Hash] Default construction variables for the builder.
def default_variables(env)
{
'LD' => nil,
'OBJSUFFIX' => '.o',
'PROGSUFFIX' => (Object.const_get("RUBY_PLATFORM") =~ /mingw|cygwin/ ? ".exe" : ""),
'LD' => nil,
'LIBSUFFIX' => '.a',
'LDFLAGS' => [],
'LIBPATH' => [],
'LIBDIRPREFIX' => '-L',
'LIBLINKPREFIX' => '-l',
'LIBS' => [],
'LDCOM' => ['$LD', '-o', '$_TARGET', '$LDFLAGS', '$_SOURCES', '-L$[LIBPATH]', '-l$[LIBS]']
'LDCMD' => ['${LD}', '-o', '${_TARGET}', '${LDFLAGS}', '${_SOURCES}', '${LIBDIRPREFIX}${LIBPATH}', '${LIBLINKPREFIX}${LIBS}']
}
end
def run(target, sources, cache, env, vars = {})
# Create a BuildTarget object for this build target.
#
# The build target filename is given a ".exe" suffix if Rscons is
# executing on a Windows platform and no other suffix is given.
#
# @param options [Hash] Options to create the BuildTarget with.
# @option options [Environment] :env
# The Environment.
# @option options [String] :target
# The user-supplied target name.
# @option options [Array<String>] :sources
# The user-supplied source file name(s).
#
# @return [BuildTarget]
def create_build_target(options)
my_options = options.dup
unless my_options[:target] =~ /\./
my_options[:target] += options[:env].expand_varref("${PROGSUFFIX}")
end
super(my_options)
end
# Run the builder to produce a build target.
#
# @param target [String] Target file name.
# @param sources [Array<String>] Source file name(s).
# @param cache [Cache] The Cache object.
# @param env [Environment] The Environment executing the builder.
# @param vars [Hash,VarSet] Extra construction variables.
#
# @return [String,false]
# Name of the target file on success or false on failure.
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
use_cxx = sources.map do |s|
s.has_suffix?(env['CXXSUFFIX'])
end.any?
ld_alt = use_cxx ? env['CXX'] : env['CC']
objects = env.build_sources(sources, env.expand_varref(["${OBJSUFFIX}", "${LIBSUFFIX}"], vars).flatten, cache, vars)
return false unless objects
ld = env.expand_varref("${LD}", vars)
ld = if ld != ""
ld
elsif sources.find {|s| s.end_with?(*env.expand_varref("${DSUFFIX}", vars))}
"${DC}"
elsif sources.find {|s| s.end_with?(*env.expand_varref("${CXXSUFFIX}", vars))}
"${CXX}"
else
"${CC}"
end
vars = vars.merge({
'_TARGET' => target,
'_SOURCES' => objects,
'LD' => env['LD'] || ld_alt,
'LD' => ld,
})
command = env.build_command(env['LDCOM'], vars)
command = env.build_command("${LDCMD}", vars)
standard_build("LD #{target}", target, command, objects, env, cache)
end
end
+155 -92
View File
@@ -1,92 +1,103 @@
require 'yaml'
require 'fileutils'
require 'digest/md5'
require 'set'
require 'rscons/version'
require "digest/md5"
require "fileutils"
require "json"
require "set"
require "singleton"
require "rscons/version"
module Rscons
# The Cache class keeps track of file checksums, build target commands and
# dependencies in a YAML file which persists from one invocation to the next.
# dependencies in a JSON file which persists from one invocation to the next.
# Example cache:
# {
# version: '1.2.3',
# targets: {
# 'program' => {
# 'checksum' => 'A1B2C3D4',
# 'command' => ['gcc', '-o', 'program', 'program.o'],
# 'deps' => [
# "version" => "1.2.3",
# "targets" => {
# "program" => {
# "checksum" => "A1B2C3D4",
# "command" => "13543518FE",
# "deps" => [
# {
# 'fname' => 'program.o',
# 'checksum' => '87654321',
# }
# "fname" => "program.o",
# "checksum" => "87654321",
# },
# ],
# }
# 'program.o' => {
# 'checksum' => '87654321',
# 'command' => ['gcc', '-c', '-o', 'program.o', 'program.c'],
# 'deps' => [
# "user_deps" => [
# {
# 'fname' => 'program.c',
# 'checksum' => '456789ABC',
# "fname" => "lscript.ld",
# "checksum" => "77551133",
# },
# ],
# },
# "program.o" => {
# "checksum" => "87654321",
# "command" => "98765ABCD",
# "deps" => [
# {
# "fname" => "program.c",
# "checksum" => "456789ABC",
# },
# {
# 'fname' => 'program.h',
# 'checksum' => '7979764643',
# }
# ]
# }
# }
# directories: {
# 'build' => true,
# 'build/one' => true,
# 'build/two' => true,
# "fname" => "program.h",
# "checksum" => "7979764643",
# },
# ],
# "user_deps" => [],
# }
# },
# "directories" => {
# "build" => true,
# "build/one" => true,
# "build/two" => true,
# },
# }
class Cache
#### Constants
include Singleton
# Name of the file to store cache information in
CACHE_FILE = '.rsconscache'
#### Class Methods
# Remove the cache file
def self.clear
FileUtils.rm_f(CACHE_FILE)
end
#### Instance Methods
CACHE_FILE = ".rsconscache"
# Create a Cache object and load in the previous contents from the cache
# file.
def initialize
@cache = YAML.load(File.read(CACHE_FILE)) rescue {}
unless @cache.is_a?(Hash)
$stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
@cache = {}
initialize!
end
@cache[:targets] ||= {}
@cache[:directories] ||= {}
@cache[:version] ||= VERSION
# Remove the cache file.
def clear
FileUtils.rm_f(CACHE_FILE)
initialize!
end
# Clear the cached file checksums.
def clear_checksum_cache!
@lookup_checksums = {}
end
# Write the cache to disk to be loaded next time.
def write
File.open(CACHE_FILE, 'w') do |fh|
fh.puts(YAML.dump(@cache))
if @dirty || (@cache["version"] != VERSION)
@cache["version"] = VERSION
File.open(CACHE_FILE, "w") do |fh|
fh.puts(JSON.dump(@cache))
end
end
@dirty = false
end
# Check if a target is up to date
# @param target [String] The name of the target file.
# @param command [Array] The command used to build the target.
# Check if target(s) are up to date.
#
# @param targets [String, Array] The name(s) of the target file(s).
# @param command [String, Array] The command used to build the target.
# @param deps [Array] List of the target's dependency files.
# @param options [Hash] Optional options. Can contain the following keys:
# :strict_deps::
# @param env [Environment] The Rscons::Environment.
# @param options [Hash] Optional options.
# @option options [Boolean] :strict_deps
# Only consider a target up to date if its list of dependencies is
# exactly equal (including order) to the cached list of dependencies
# @return true value if the target is up to date, meaning that:
#
# @return [Boolean]
# True value if the targets are all up to date, meaning that,
# for each target:
# - the target exists on disk
# - the cache has information for the target
# - the target's checksum matches its checksum when it was last built
@@ -96,90 +107,142 @@ module Rscons
# exactly equal to those cached
# - each cached dependency file's current checksum matches the checksum
# 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
return false unless File.exists?(target)
# 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)
return false unless @cache["targets"][target]["checksum"] == lookup_checksum(target)
# command used to build target must be identical
return false unless @cache[:targets][target][:command] == command
return false unless @cache["targets"][target]["command"] == Digest::MD5.hexdigest(command.inspect)
cached_deps = @cache[:targets][target][:deps].map { |dc| dc[:fname] }
cached_deps = @cache["targets"][target]["deps"] || []
cached_deps_fnames = cached_deps.map { |dc| dc["fname"] }
if options[:strict_deps]
# depedencies passed in must exactly equal those in the cache
return false unless deps == cached_deps
return false unless deps == cached_deps_fnames
else
# 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
# 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
@cache[:targets][target][:deps].map do |dep_cache|
dep_cache[:checksum] == lookup_checksum(dep_cache[:fname])
end.all?
(cached_deps + cached_user_deps).each do |dep_cache|
return false unless dep_cache["checksum"] == lookup_checksum(dep_cache["fname"])
end
end
# Store cache information about a target built by a builder
# @param target [String] The name of the target.
# @param command [Array] The command used to build the target.
true
end
# 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.
def register_build(target, command, deps)
@cache[:targets][target] = {
command: command,
checksum: calculate_checksum(target),
deps: deps.map do |dep|
# @param env [Environment] The {Rscons::Environment}.
#
# @return [void]
def register_build(targets, command, deps, env)
Array(targets).each do |target|
@cache["targets"][target] = {
"command" => Digest::MD5.hexdigest(command.inspect),
"checksum" => calculate_checksum(target),
"deps" => deps.map do |dep|
{
fname: dep,
checksum: lookup_checksum(dep),
"fname" => dep,
"checksum" => lookup_checksum(dep),
}
end,
"user_deps" => (env.get_user_deps(target) || []).map do |dep|
{
"fname" => dep,
"checksum" => lookup_checksum(dep),
}
end,
}
@dirty = true
end
}
end
# Return a list of targets that have been built
# Return a list of targets that have been built.
#
# @return [Array<String>] List of targets that have been built.
def targets
@cache[:targets].keys
@cache["targets"].keys
end
# Make any needed directories and record the ones that are created for
# removal upon a "clean" operation.
#
# @param path [String] Directory to create.
#
# @return [void]
def mkdir_p(path)
parts = path.split(/[\\\/]/)
(0..parts.size).each do |i|
subpath = File.join(*parts[0, i + 1]).encode(__ENCODING__)
parts.each_index do |i|
next if parts[i] == ""
subpath = File.join(*parts[0, i + 1])
unless File.exists?(subpath)
FileUtils.mkdir(subpath)
unless @cache[:directories].include?(subpath)
@cache[:directories][subpath] = true
end
@cache["directories"][subpath] = true
@dirty = true
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.
#
# @return [Array<String>]
# List of directories which were created as a part of the build.
def directories
@cache[:directories].keys
@cache["directories"].keys
end
# Private Instance Methods
private
# Create a Cache object and load in the previous contents from the cache
# file.
def initialize!
@cache = JSON.load(File.read(CACHE_FILE)) rescue {}
unless @cache.is_a?(Hash)
$stderr.puts "Warning: #{CACHE_FILE} was corrupt. Contents:\n#{@cache.inspect}"
@cache = {}
end
@cache["targets"] ||= {}
@cache["directories"] ||= {}
@lookup_checksums = {}
@dirty = false
end
# Return a file's checksum, or the previously calculated checksum for
# the same file
# the same file.
#
# @param file [String] The file name.
#
# @return [String] The file's checksum.
def lookup_checksum(file)
@lookup_checksums[file] || calculate_checksum(file)
end
# Calculate and return a file's checksum
# Calculate and return a file's checksum.
#
# @param file [String] The file name.
#
# @return [String] The file's checksum.
def calculate_checksum(file)
@lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, {mode: 'rb'})).encode(__ENCODING__) rescue ''
@lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file, mode: "rb")) rescue ""
end
end
end
+497 -122
View File
@@ -1,40 +1,57 @@
require 'set'
require 'fileutils'
require "fileutils"
require "set"
require "shellwords"
module Rscons
# The Environment class is the main programmatic interface to RScons. It
# The Environment class is the main programmatic interface to Rscons. It
# contains a collection of construction variables, options, builders, and
# rules for building targets.
class Environment
# [Array] of {Builder} objects.
# @return [Hash] Set of \{"builder_name" => builder_object} pairs.
attr_reader :builders
# :command, :short, or :off
attr_accessor :echo
# @return [String, nil] The build root.
attr_reader :build_root
# Set the build root.
#
# @param build_root [String] The build root.
def build_root=(build_root)
@build_root = build_root
@build_root.gsub!('\\', '/') if @build_root
end
# Create an Environment object.
# @param variables [Hash]
# The variables hash can contain construction variables, which are
# uppercase strings (such as "CC" or "LDFLAGS"), user variables, which
# are lowercase strings (such as "sources"), and RScons options, which
# are lowercase symbols (such as :echo).
#
# @param options [Hash]
# @option options [Symbol] :echo
# :command, :short, or :off (default :short)
# @option options [String] :build_root
# Build root directory (default nil)
# @option options [Boolean] :exclude_builders
# Whether to omit adding default builders (default false)
#
# If a block is given, the Environment object is yielded to the block and
# when the block returns, the {#process} method is automatically called.
def initialize(variables = {})
@varset = VarSet.new(variables)
def initialize(options = {})
@varset = VarSet.new
@targets = {}
@user_deps = {}
@builders = {}
@build_dirs = []
@varset[:exclude_builders] ||= []
unless @varset[:exclude_builders] == :all
exclude_builders = Set.new(@varset[:exclude_builders] || [])
DEFAULT_BUILDERS.each do |builder_class|
unless exclude_builders.include?(builder_class.short_name)
@build_hooks = []
unless options[:exclude_builders]
DEFAULT_BUILDERS.each do |builder_class_name|
builder_class = Builders.const_get(builder_class_name)
builder_class or raise "Could not find builder class #{builder_class_name}"
add_builder(builder_class.new)
end
end
end
(@varset[:builders] || []).each do |builder|
add_builder(builder)
end
@varset[:echo] ||= :command
@echo = options[:echo] || :short
@build_root = options[:build_root]
if block_given?
yield self
@@ -43,18 +60,53 @@ module Rscons
end
# Make a copy of the Environment object.
# The cloned environment will contain a copy of all environment options,
# construction variables, builders, and build directories. It will not
# contain a copy of the targets.
#
# By default, a cloned environment will contain a copy of all environment
# options, construction variables, and builders, but not a copy of the
# targets, build hooks, build directories, or the build root.
#
# Exactly which items are cloned are controllable via the optional :clone
# parameter, which can be :none, :all, or a set or array of any of the
# following:
# - :variables to clone construction variables (on by default)
# - :builders to clone the builders (on by default)
# - :build_root to clone the build root (off by default)
# - :build_dirs to clone the build directories (off by default)
# - :build_hooks to clone the build hooks (off by default)
#
# If a block is given, the Environment object is yielded to the block and
# when the block returns, the {#process} method is automatically called.
def clone(variables = {})
env = Environment.new()
#
# Any options that #initialize receives can also be specified here.
#
# @return a new {Environment} object.
def clone(options = {})
clone = options[:clone] || Set[:variables, :builders]
clone = Set[:variables, :builders, :build_root, :build_dirs, :build_hooks] if clone == :all
clone = Set[] if clone == :none
clone = Set.new(clone) if clone.is_a?(Array)
clone.delete(:builders) if options[:exclude_builders]
env = self.class.new(
echo: options[:echo] || @echo,
build_root: options[:build_root],
exclude_builders: true)
if clone.include?(:builders)
@builders.each do |builder_name, builder|
env.add_builder(builder)
end
env.append(@varset)
env.append(variables)
end
env.append(@varset) if clone.include?(:variables)
env.build_root = @build_root if clone.include?(:build_root)
if clone.include?(:build_dirs)
@build_dirs.each do |src_dir, obj_dir|
env.build_dir(src_dir, obj_dir)
end
end
if clone.include?(:build_hooks)
@build_hooks.each do |build_hook_block|
env.add_build_hook(&build_hook_block)
end
end
if block_given?
yield env
@@ -64,8 +116,12 @@ module Rscons
end
# Add a {Builder} object to the Environment.
#
# @param builder [Builder] The {Builder} object to add.
#
# @return [void]
def add_builder(builder)
@builders[builder.class.short_name] = builder
@builders[builder.name] = builder
var_defs = builder.default_variables(self)
if var_defs
var_defs.each_pair do |var, val|
@@ -74,146 +130,492 @@ module Rscons
end
end
# Add a build hook to the Environment.
#
# @yield [build_op]
# Invoke the given block with the current build operation.
# @yieldparam build_op [Hash]
# Hash with keys:
# - :builder - The builder object in use.
# - :target - Target file name.
# - :sources - List of source file(s).
# - :vars - Set of construction variable values in use.
# - :env - The Environment invoking the builder.
#
# @return [void]
def add_build_hook(&block)
@build_hooks << block
end
# Specify a build directory for this Environment.
#
# Source files from src_dir will produce object files under obj_dir.
#
# @param src_dir [String] Path to the source directory.
# @param obj_dir [String] Path to the object directory.
#
# @return [void]
def build_dir(src_dir, obj_dir)
src_dir = src_dir.gsub('\\', '/') if src_dir.is_a?(String)
obj_dir = obj_dir.gsub('\\', '/')
@build_dirs << [src_dir, obj_dir]
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.
# It also creates any parent directories needed to be able to open and
# write to the output file.
#
# @param source_fname [String] Source file name.
# @param suffix [String] Suffix, including "." if desired.
#
# @return [String]
# The file name to be built from +source_fname+ with suffix +suffix+.
def get_build_fname(source_fname, suffix)
build_fname = source_fname.set_suffix(suffix).gsub('\\', '/')
@build_dirs.each do |src_dir, obj_dir|
build_fname = Rscons.set_suffix(source_fname, suffix).gsub('\\', '/')
found_match = @build_dirs.find do |src_dir, obj_dir|
if src_dir.is_a?(Regexp)
build_fname.sub!(src_dir, obj_dir)
else
build_fname.sub!(/^#{src_dir}\//, "#{obj_dir}/")
build_fname.sub!(%r{^#{src_dir}/}, "#{obj_dir}/")
end
end
if @build_root and not found_match
unless Rscons.absolute_path?(source_fname) or build_fname.start_with?("#{@build_root}/")
build_fname = "#{@build_root}/#{build_fname}"
end
end
build_fname.gsub!('\\', '/')
end
build_fname
end
# Access a construction variable or environment option.
#
# @see VarSet#[]
def [](*args)
@varset.send(:[], *args)
end
# Set a construction variable or environment option.
#
# @see VarSet#[]=
def []=(*args)
@varset.send(:[]=, *args)
end
# Add a set of construction variables or environment options.
#
# @see VarSet#append
def append(*args)
@varset.send(:append, *args)
@varset.append(*args)
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 build targets specified in the Environment.
#
# When a block is passed to Environment.new, this method is automatically
# called after the block returns.
#
# @return [void]
def process
cache = Cache.new
targets_processed = Set.new
unless @targets.empty?
expand_paths!
cache = Cache.instance
cache.clear_checksum_cache!
targets_processed = {}
process_target = proc do |target|
if @targets[target][:source].map do |src|
targets_processed.include?(src) or not @targets.include?(src) or process_target.call(src)
end.all?
@targets[target][:builder].run(target,
@targets[target][:source],
targets_processed[target] ||= begin
@targets[target][:sources].each do |src|
if @targets.include?(src) and not targets_processed.include?(src)
process_target.call(src)
end
end
result = run_builder(@targets[target][:builder],
target,
@targets[target][:sources],
cache,
self,
@targets[target][:vars] || {})
else
false
end
end
@targets.each do |target, info|
next if targets_processed.include?(target)
unless process_target.call(target)
cache.write
unless result
raise BuildError.new("Failed to build #{target}")
end
result
end
end
begin
@targets.each do |target, target_params|
process_target.call(target)
end
ensure
cache.write
end
# Build a command line from the given template, resolving references to
# variables using the Environment's construction variables and any extra
# variables specified.
# @param command_template [Array] template for the command with variable
# references
# @param extra_vars [Hash, VarSet] extra variables to use in addition to
# (or replace) the Environment's construction variables when building
# the command
def build_command(command_template, extra_vars)
@varset.merge(extra_vars).expand_varref(command_template)
end
end
# Execute a builder command
# @param short_desc [String] Message to print if the Environment's :echo
# Clear all targets registered for the Environment.
def clear_targets
@targets = {}
end
# Expand a construction variable reference.
#
# @param varref [Array, String] Variable reference to expand.
# @param extra_vars [Hash, VarSet]
# Extra variables to use in addition to (or replace) the Environment's
# construction variables when expanding the variable reference.
#
# @return [Array, String] Expansion of the variable reference.
def expand_varref(varref, extra_vars = nil)
vars = if extra_vars.nil?
@varset
else
@varset.merge(extra_vars)
end
lambda_args = [env: self, vars: vars]
vars.expand_varref(varref, lambda_args)
end
alias_method :build_command, :expand_varref
# Execute a builder command.
#
# @param short_desc [String] Message to print if the Environment's echo
# mode is set to :short
# @param command [Array] The command to execute.
# @param options [Hash] Optional options to pass to {Kernel#system}.
# @param options [Hash] Optional options, possible keys:
# - :env - environment Hash to pass to Kernel#system.
# - :options - options Hash to pass to Kernel#system.
#
# @return [true,false,nil] Return value from Kernel.system().
def execute(short_desc, command, options = {})
print_command = proc do
puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
end
if @varset[:echo] == :command
if @echo == :command
print_command.call
elsif @varset[:echo] == :short
elsif @echo == :short
puts short_desc
end
system(*command, options).tap do |result|
unless result or @varset[:echo] == :command
env_args = options[:env] ? [options[:env]] : []
options_args = options[:options] ? [options[:options]] : []
system(*env_args, *Rscons.command_executer, *command, *options_args).tap do |result|
unless result or @echo == :command
$stdout.write "Failed command was: "
print_command.call
end
end
end
alias_method :orig_method_missing, :method_missing
# Define a build target.
#
# @param method [Symbol] Method name.
# @param args [Array] Method arguments.
#
# @return [BuildTarget]
# The {BuildTarget} object registered, if the method called is a
# {Builder}.
def method_missing(method, *args)
if @builders.has_key?(method.to_s)
target, source, vars, *rest = args
target, sources, vars, *rest = args
unless vars.nil? or vars.is_a?(Hash) or vars.is_a?(VarSet)
raise "Unexpected construction variable set: #{vars.inspect}"
end
source = [source] unless source.is_a?(Array)
@targets[target] = {
builder: @builders[method.to_s],
source: source,
vars: vars,
args: rest,
}
sources = Array(sources)
builder = @builders[method.to_s]
build_target = builder.create_build_target(env: self, target: target, sources: sources)
add_target(build_target.to_s, builder, sources, vars, rest)
build_target
else
orig_method_missing(method, *args)
super
end
end
# Add a build target.
#
# @param target [String] Build target file name.
# @param builder [Builder] The {Builder} to use to build the target.
# @param sources [Array<String>] Source file name(s).
# @param vars [Hash] Construction variable overrides.
# @param args [Object] Any extra arguments passed to the {Builder}.
#
# @return [void]
def add_target(target, builder, sources, vars, args)
@targets[target] = {
builder: builder,
sources: sources,
vars: vars,
args: args,
}
end
# Manually record a given target as depending on the specified files.
#
# @param target [String,BuildTarget] Target file.
# @param user_deps [Array<String>] Dependency files.
#
# @return [void]
def depends(target, *user_deps)
target = target.to_s
@user_deps[target] ||= []
@user_deps[target] = (@user_deps[target] + user_deps).uniq
end
# Return the list of user dependencies for a given target.
#
# @param target [String] Target file name.
#
# @return [Array<String>,nil]
# List of user-specified dependencies for the target, or nil if none were
# specified.
def get_user_deps(target)
@user_deps[target]
end
# Build a list of source files into files containing one of the suffixes
# given by suffixes.
#
# This method is used internally by Rscons builders.
#
# @param sources [Array] List of source files to build.
# @param suffixes [Array] List of suffixes to try to convert source files into.
# @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder.
#
# @return [Array<String>] List of the converted file name(s).
def build_sources(sources, suffixes, cache, vars)
sources.map do |source|
if source.end_with?(*suffixes)
source
else
converted = nil
suffixes.each do |suffix|
converted_fname = get_build_fname(source, suffix)
builder = @builders.values.find { |b| b.produces?(converted_fname, source, self) }
if builder
converted = run_builder(builder, converted_fname, [source], cache, vars)
return nil unless converted
break
end
end
converted or raise "Could not find a builder to handle #{source.inspect}."
end
end
end
# Invoke a builder to build the given target based on the given sources.
#
# @param builder [Builder] The Builder to use.
# @param target [String] The target output file.
# @param sources [Array] List of source files.
# @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder.
#
# @return [String,false] Return value from the {Builder}'s +run+ method.
def run_builder(builder, target, sources, cache, vars)
vars = @varset.merge(vars)
@build_hooks.each do |build_hook_block|
build_operation = {
builder: builder,
target: target,
sources: sources,
vars: vars,
env: self,
}
build_hook_block.call(build_operation)
end
builder.run(target, sources, cache, self, vars)
end
# Expand a path to be relative to the Environment's build root.
#
# Paths beginning with "^/" are expanded by replacing "^" with the
# Environment's build root.
#
# @param path [String] The path to expand.
#
# @return [String] The expanded path.
def expand_path(path)
path.sub(%r{^\^(?=[\\/])}, @build_root)
end
# Execute a command using the system shell.
#
# The shell is automatically determined but can be overridden by the SHELL
# construction variable. If the SHELL construction variable is specified,
# the flag to pass to the shell is automatically dtermined but can be
# overridden by the SHELLFLAG construction variable.
#
# @param command [String] Command to execute.
#
# @return [String] The command's standard output.
def shell(command)
shell_cmd =
if self["SHELL"]
flag = self["SHELLFLAG"] || (self["SHELL"] == "cmd" ? "/c" : "-c")
[self["SHELL"], flag]
else
Rscons.get_system_shell
end
IO.popen([*shell_cmd, command]) do |io|
io.read
end
end
# @!method parse_flags(flags)
# @!method parse_flags!(flags)
#
# Parse command-line flags for compilation/linking options into separate
# construction variables.
#
# The parsed construction variables are returned in a Hash instead of
# merging them directly to the Environment. They can be merged with
# {#merge_flags}. The {#parse_flags!} version immediately merges the parsed
# flags as well.
#
# Example:
# # Import FreeType build options
# env.parse_flags!("!freetype-config --cflags --libs")
#
# @param flags [String]
# String containing the flags to parse, or if the flags string begins
# with "!", a shell command to execute using {#shell} to obtain the
# flags to parse.
#
# @return [Hash] Set of construction variables to append.
def parse_flags(flags)
if flags =~ /^!(.*)$/
flags = shell($1)
end
rv = {}
words = Shellwords.split(flags)
skip = false
words.each_with_index do |word, i|
if skip
skip = false
next
end
append = lambda do |var, val|
rv[var] ||= []
rv[var] += val
end
handle = lambda do |var, val|
if val.nil? or val.empty?
val = words[i + 1]
skip = true
end
if val and not val.empty?
append[var, [val]]
end
end
if word == "-arch"
if val = words[i + 1]
append["CCFLAGS", ["-arch", val]]
append["LDFLAGS", ["-arch", val]]
end
skip = true
elsif word =~ /^#{self["CPPDEFPREFIX"]}(.*)$/
handle["CPPDEFINES", $1]
elsif word == "-include"
if val = words[i + 1]
append["CCFLAGS", ["-include", val]]
end
skip = true
elsif word == "-isysroot"
if val = words[i + 1]
append["CCFLAGS", ["-isysroot", val]]
append["LDFLAGS", ["-isysroot", val]]
end
skip = true
elsif word =~ /^#{self["INCPREFIX"]}(.*)$/
handle["CPPPATH", $1]
elsif word =~ /^#{self["LIBLINKPREFIX"]}(.*)$/
handle["LIBS", $1]
elsif word =~ /^#{self["LIBDIRPREFIX"]}(.*)$/
handle["LIBPATH", $1]
elsif word == "-mno-cygwin"
append["CCFLAGS", [word]]
append["LDFLAGS", [word]]
elsif word == "-mwindows"
append["LDFLAGS", [word]]
elsif word == "-pthread"
append["CCFLAGS", [word]]
append["LDFLAGS", [word]]
elsif word =~ /^-std=/
append["CFLAGS", [word]]
elsif word =~ /^-Wa,(.*)$/
append["ASFLAGS", $1.split(",")]
elsif word =~ /^-Wl,(.*)$/
append["LDFLAGS", $1.split(",")]
elsif word =~ /^-Wp,(.*)$/
append["CPPFLAGS", $1.split(",")]
elsif word.start_with?("-")
append["CCFLAGS", [word]]
elsif word.start_with?("+")
append["CCFLAGS", [word]]
append["LDFLAGS", [word]]
else
append["LIBS", [word]]
end
end
rv
end
def parse_flags!(flags)
flags = parse_flags(flags)
merge_flags(flags)
flags
end
# Merge construction variable flags into this Environment's construction
# variables.
#
# This method does the same thing as {#append}, except that Array values in
# +flags+ are appended to the end of Array construction variables instead
# of replacing their contents.
#
# @param flags [Hash]
# Set of construction variables to merge into the current Environment.
# This can be the value (or a modified version) returned by
# {#parse_flags}.
#
# @return [void]
def merge_flags(flags)
flags.each_pair do |key, val|
if self[key].is_a?(Array) and val.is_a?(Array)
self[key] += val
else
self[key] = val
end
end
end
private
# Expand target and source paths before invoking builders.
#
# This method expand construction variable references in the target and
# source file names before passing them to the builder. It also expands
# "^/" prefixes to the Environment's build root if a build root is defined.
#
# @return [void]
def expand_paths!
@targets = @targets.reduce({}) do |result, (target, target_params)|
sources = target_params[:sources].map do |source|
source = expand_path(source) if @build_root
expand_varref(source)
end
target = expand_path(target) if @build_root
target = expand_varref(target)
result[target] = target_params.merge(sources: sources)
result
end
end
# Parse dependencies for a given target from a Makefile.
# This method is used internally by RScons builders.
#
# 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 parse_makefile_deps(mf_fname, target)
#
# @return [Array<String>] Paths of dependency files.
def self.parse_makefile_deps(mf_fname, target)
deps = []
buildup = ''
File.read(mf_fname).each_line do |line|
@@ -232,32 +634,5 @@ module Rscons
end
deps
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.
# 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 = builder.run(converted_fname, [source], cache, self, vars)
return nil unless converted
break
end
end
converted or raise "Could not find a builder to handle #{source.inspect}."
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
+124 -51
View File
@@ -1,86 +1,159 @@
module Rscons
# This class represents a collection of variables which can be accessed
# as certain types
# This class represents a collection of variables which supports efficient
# deep cloning.
class VarSet
# The underlying hash
attr_reader :vars
# Create a VarSet
# Create a VarSet.
#
# @param vars [Hash] Optional initial variables.
def initialize(vars = {})
if vars.is_a?(VarSet)
@vars = vars.clone.vars
else
@vars = vars
end
@my_vars = {}
@coa_vars = []
append(vars)
end
# Access the value of variable as a particular type
# Access the value of variable.
#
# @param key [String, Symbol] The variable name.
# @param type [Symbol, nil] Optional specification of the type desired.
# If the variable is a String and type is :array, a 1-element array with
# 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 val.is_a?(String)
[val]
elsif type == :string and val.is_a?(Array)
val.first
#
# @return [Object] The variable's value.
def [](key)
if @my_vars.include?(key)
@my_vars[key]
else
val
@coa_vars.each do |coa_vars|
if coa_vars.include?(key)
@my_vars[key] = deep_dup(coa_vars[key])
return @my_vars[key]
end
end
nil
end
end
# Assign a value to a variable.
#
# @param key [String, Symbol] The variable name.
# @param val [Object] The value.
#
# @param val [Object] The value to set.
def []=(key, val)
@vars[key] = val
@my_vars[key] = val
end
# Add or overwrite a set of variables
# Check if the VarSet contains a variable.
#
# @param key [String, Symbol] The variable name.
#
# @return [Boolean] Whether the VarSet contains the variable.
def include?(key)
if @my_vars.include?(key)
true
else
@coa_vars.find do |coa_vars|
coa_vars.include?(key)
end
end
end
# Add or overwrite a set of variables.
#
# @param values [VarSet, Hash] New set of variables.
#
# @return [self]
def append(values)
values = values.vars if values.is_a?(VarSet)
@vars.merge!(values)
coa!
if values.is_a?(VarSet)
values.send(:coa!)
@coa_vars = values.instance_variable_get(:@coa_vars) + @coa_vars
else
@my_vars = deep_dup(values)
end
self
end
# Create a new VarSet object based on the first merged with other.
#
# @param other [VarSet, Hash] Other variables to add or overwrite.
#
# @return [VarSet] The newly created VarSet.
def merge(other = {})
VarSet.new(Marshal.load(Marshal.dump(@vars))).append(other)
coa!
varset = self.class.new
varset.instance_variable_set(:@coa_vars, @coa_vars.dup)
varset.append(other)
end
alias_method :clone, :merge
# Replace "$" variable references in varref with the variables values,
# recursively.
# @param varref [String, Array] Value containing references to variables.
def expand_varref(varref)
if varref.is_a?(Array)
varref.map do |ent|
expand_varref(ent)
end.flatten
else
if varref =~ /^(.*)\$\[(\w+)\](.*)$/
# expand array with given prefix, suffix
# Replace "$\{var}" variable references in varref with the expanded
# variables' values, recursively.
#
# @param varref [nil, String, Array, Proc]
# Value containing references to variables.
# @param lambda_args [Array]
# Arguments to pass to any lambda variable values to be expanded.
#
# @return [nil, String, Array]
# Expanded value with "$\{var}" variable references replaced.
def expand_varref(varref, lambda_args)
if varref.is_a?(String)
if varref =~ /^(.*)\$\{([^}]+)\}(.*)$/
prefix, varname, suffix = $1, $2, $3
varval = expand_varref(@vars[varname])
unless varval.is_a?(Array)
raise "Array expected for $#{varname}"
varval = expand_varref(self[varname], lambda_args)
if varval.is_a?(String) or varval.nil?
expand_varref("#{prefix}#{varval}#{suffix}", lambda_args)
elsif varval.is_a?(Array)
varval.map {|vv| expand_varref("#{prefix}#{vv}#{suffix}", lambda_args)}.flatten
else
raise "I do not know how to expand a variable reference to a #{varval.class.name} (from #{varname.inspect} => #{self[varname].inspect})"
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
varref
end
elsif varref.is_a?(Array)
varref.map do |ent|
expand_varref(ent, lambda_args)
end.flatten
elsif varref.is_a?(Proc)
expand_varref(varref[*lambda_args], lambda_args)
elsif varref.nil?
nil
else
raise "Unknown varref type: #{varref.class} (#{varref.inspect})"
end
end
private
# Move all VarSet variables into the copy-on-access list.
#
# @return [void]
def coa!
unless @my_vars.empty?
@coa_vars.unshift(@my_vars)
@my_vars = {}
end
end
# Create a deep copy of an object.
#
# Only objects which are of type String, Array, or Hash are deep copied.
# Any other object just has its referenced copied.
#
# @param obj [Object] Object to deep copy.
#
# @return [Object] Deep copied value.
def deep_dup(obj)
obj_class = obj.class
if obj_class == Hash
obj.reduce({}) do |result, (k, v)|
result[k] = deep_dup(v)
result
end
elsif obj_class == Array
obj.map { |v| deep_dup(v) }
elsif obj_class == String
obj.dup
else
obj
end
end
end
+1 -1
View File
@@ -1,4 +1,4 @@
module Rscons
# gem version
VERSION = "0.0.10"
VERSION = "1.6.0"
end
+8 -10
View File
@@ -1,30 +1,28 @@
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib', __FILE__)
lib = File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'rscons/version'
require "rscons/version"
Gem::Specification.new do |gem|
gem.name = "rscons"
gem.version = Rscons::VERSION
gem.authors = ["Josh Holtrop"]
gem.email = ["jholtrop@gmail.com"]
gem.description = %q{Software construction library inspired by SCons and implemented in Ruby}
gem.description = %q{Software construction library inspired by SCons and implemented in Ruby.}
gem.summary = %q{Software construction library inspired by SCons and implemented in Ruby}
gem.homepage = "https://github.com/holtrop/rscons"
gem.license = 'MIT'
gem.license = "MIT"
gem.files = `git ls-files`.split($/)
gem.files = Dir["{bin,assets,lib}/**/*", "*.gemspec", "LICENSE.txt"]
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
gem.require_paths = ["lib"]
gem.add_development_dependency "rspec-core"
gem.add_development_dependency "rspec-mocks"
gem.add_development_dependency "rspec-expectations"
gem.add_dependency "json", "~> 1.0"
gem.add_development_dependency "rspec"
gem.add_development_dependency "rake"
gem.add_development_dependency "simplecov"
gem.add_development_dependency "json"
gem.add_development_dependency 'rdoc'
gem.add_development_dependency "yard"
gem.add_development_dependency "redcarpet"
end
+459 -92
View File
@@ -1,27 +1,58 @@
require 'fileutils'
class Dir
class << self
alias_method :orig_bracket, :[]
end
def self.[](*args)
orig_bracket(*args).sort
end
end
describe Rscons do
BUILD_TEST_RUN_DIR = "build_test_run"
def rm_rf(dir)
FileUtils.rm_rf(dir)
if File.exists?(dir)
sleep 0.2
FileUtils.rm_rf(dir)
if File.exists?(dir)
sleep 0.5
FileUtils.rm_rf(dir)
if File.exists?(dir)
sleep 1.0
FileUtils.rm_rf(dir)
if File.exists?(dir)
raise "Could not remove #{dir}"
end
end
end
end
end
before(:all) do
FileUtils.rm_rf('build_tests_run')
rm_rf(BUILD_TEST_RUN_DIR)
@owd = Dir.pwd
end
after(:each) do
Dir.chdir(@owd)
FileUtils.rm_rf('build_tests_run')
before(:each) do
@saved_stdout = $stdout
$stdout = StringIO.new
@saved_stderr = $stderr
$stderr = StringIO.new
end
def build_testdir
if File.exists?("build.rb")
system("ruby -I #{@owd}/lib -r rscons build.rb > build.out")
end
get_build_output
after(:each) do
$stdout = @saved_stdout
$stderr = @saved_stderr
Dir.chdir(@owd)
rm_rf(BUILD_TEST_RUN_DIR)
end
def test_dir(build_test_directory)
FileUtils.cp_r("build_tests/#{build_test_directory}", 'build_tests_run')
Dir.chdir("build_tests_run")
build_testdir
FileUtils.cp_r("build_tests/#{build_test_directory}", BUILD_TEST_RUN_DIR)
Dir.chdir(BUILD_TEST_RUN_DIR)
end
def file_sub(fname)
@@ -30,13 +61,16 @@ describe Rscons do
contents.each_line do |line|
replaced += yield(line)
end
File.open(fname, 'w') do |fh|
File.open(fname, 'wb') do |fh|
fh.write(replaced)
end
end
def get_build_output
File.read('build.out').lines.map(&:strip)
def lines
rv = ($stdout.string + $stderr.string).lines.map(&:rstrip)
$stdout.string = ""
$stderr.string = ""
rv
end
###########################################################################
@@ -45,154 +79,487 @@ describe Rscons do
it 'builds a C program with one source file' do
test_dir('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'])
end
expect(File.exists?('simple.o')).to be_truthy
expect(`./simple`).to eq "This is a simple C program\n"
end
it 'prints commands as they are executed' do
lines = test_dir('simple')
lines.should == [
test_dir('simple')
env = Rscons::Environment.new(echo: :command) do |env|
env["LD"] = "gcc"
env.Program('simple', Dir['*.c'])
end
expect(lines).to eq [
'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
'gcc -o simple simple.o',
"gcc -o simple#{env["PROGSUFFIX"]} simple.o",
]
end
it 'prints short representations of the commands being executed' do
lines = test_dir('header')
lines.should == [
test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
expect(lines).to eq [
'CC header.o',
'LD header',
"LD header#{env["PROGSUFFIX"]}",
]
end
it 'builds a C program with one source file and one header file' do
test_dir('header')
File.exists?('header.o').should be_true
`./header`.should == "The value is 2\n"
Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
expect(File.exists?('header.o')).to be_truthy
expect(`./header`).to eq "The value is 2\n"
end
it 'rebuilds a C module when a header it depends on changes' do
test_dir('header')
`./header`.should == "The value is 2\n"
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
expect(`./header`).to eq "The value is 2\n"
file_sub('header.h') {|line| line.sub(/2/, '5')}
build_testdir
`./header`.should == "The value is 5\n"
env.process
expect(`./header`).to eq "The value is 5\n"
end
it 'does not rebuild a C module when its dependencies have not changed' do
lines = test_dir('header')
`./header`.should == "The value is 2\n"
lines.should == [
test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
expect(`./header`).to eq "The value is 2\n"
expect(lines).to eq [
'CC header.o',
'LD header',
"LD header#{env["PROGSUFFIX"]}",
]
lines = build_testdir
lines.should == []
env.process
expect(lines).to eq []
end
it "does not rebuild a C module when only the file's timestampe has changed" do
lines = test_dir('header')
`./header`.should == "The value is 2\n"
lines.should == [
it "does not rebuild a C module when only the file's timestamp has changed" do
test_dir('header')
env = Rscons::Environment.new do |env|
env.Program('header', Dir['*.c'])
end
expect(`./header`).to eq "The value is 2\n"
expect(lines).to eq [
'CC header.o',
'LD header',
"LD header#{env["PROGSUFFIX"]}",
]
sleep 0.05
file_sub('header.c') {|line| line}
lines = build_testdir
lines.should == []
env.process
expect(lines).to eq []
end
it 're-links a program when the link flags have changed' do
lines = test_dir('simple')
lines.should == [
test_dir('simple')
env = Rscons::Environment.new(echo: :command) do |env|
env.Program('simple', Dir['*.c'])
end
expect(lines).to eq [
'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
'gcc -o simple simple.o',
"gcc -o simple#{env["PROGSUFFIX"]} simple.o",
]
file_sub('build.rb') {|line| line.sub(/.*CHANGE.FLAGS.*/, ' env["LIBS"] += ["c"]')}
lines = build_testdir
lines.should == ['gcc -o simple simple.o -lc']
e2 = Rscons::Environment.new(echo: :command) do |env|
env["LIBPATH"] += ["libdir"]
env.Program('simple', Dir['*.c'])
end
expect(lines).to eq ["gcc -o simple#{env["PROGSUFFIX"]} simple.o -Llibdir"]
end
it 'builds object files in a different build directory' do
lines = test_dir('build_dir')
`./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
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
expect(`./build_dir`).to eq "Hello from two()\n"
expect(File.exists?('build_one/one.o')).to be_truthy
expect(File.exists?('build_two/two.o')).to be_truthy
end
it 'uses build directories before build root' do
test_dir('build_dir')
env = 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
expect(lines).to eq ["CC build/one/one.o", "CC build/two/two.o", "LD build_dir#{env["PROGSUFFIX"]}"]
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.exe', Dir['src/**/*.c'])
end
expect(lines).to eq ["CC build_root/src/one/one.o", "CC build_root/src/two/two.o", "LD build_dir.exe"]
end
it "expands target and source paths starting with ^/ to be relative to the build root" do
test_dir('build_dir')
env = Rscons::Environment.new(echo: :command) do |env|
env.append('CPPPATH' => Dir['src/**/*/'])
env.build_root = "build_root"
FileUtils.mkdir_p(env.build_root)
FileUtils.mv("src/one/one.c", "build_root")
env.Object("^/one.o", "^/one.c")
env.Program('build_dir', Dir['src/**/*.c'] + ["^/one.o"])
end
expect(lines).to eq [
%q{gcc -c -o build_root/one.o -MMD -MF build_root/one.mf -Isrc/one/ -Isrc/two/ build_root/one.c},
%q{gcc -c -o build_root/src/two/two.o -MMD -MF build_root/src/two/two.mf -Isrc/one/ -Isrc/two/ src/two/two.c},
%Q{gcc -o build_dir#{env["PROGSUFFIX"]} build_root/src/two/two.o build_root/one.o},
]
end
it 'cleans built files' do
lines = test_dir('build_dir')
`./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
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
expect(`./build_dir`).to eq "Hello from two()\n"
expect(File.exists?('build_one/one.o')).to be_truthy
expect(File.exists?('build_two/two.o')).to be_truthy
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?('build').should be_false
File.exists?('src/one/one.c').should be_true
expect(File.exists?('build_one/one.o')).to be_falsey
expect(File.exists?('build_two/two.o')).to be_falsey
expect(File.exists?('build_one')).to be_falsey
expect(File.exists?('build_two')).to be_falsey
expect(File.exists?('src/one/one.c')).to be_truthy
end
it 'does not clean created directories if other non-rscons-generated files reside there' do
lines = test_dir('build_dir')
`./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" }
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
expect(`./build_dir`).to eq "Hello from two()\n"
expect(File.exists?('build_one/one.o')).to be_truthy
expect(File.exists?('build_two/two.o')).to be_truthy
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?('build').should be_true
File.exists?('src/one/one.c').should be_true
expect(File.exists?('build_one/one.o')).to be_falsey
expect(File.exists?('build_two/two.o')).to be_falsey
expect(File.exists?('build_one')).to be_falsey
expect(File.exists?('build_two')).to be_truthy
expect(File.exists?('src/one/one.c')).to be_truthy
end
it 'allows Ruby classes as custom builders to be used to construct files' do
lines = test_dir('custom_builder')
lines.should == ['CC program.o', 'LD program']
File.exists?('inc.h').should be_true
`./program`.should == "The value is 5678\n"
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
env = Rscons::Environment.new do |env|
env.add_builder(MySource.new)
env.MySource('inc.h', [])
env.Program('program', Dir['*.c'])
end
expect(lines).to eq ["CC program.o", "LD program#{env["PROGSUFFIX"]}"]
expect(File.exists?('inc.h')).to be_truthy
expect(`./program`).to eq "The value is 5678\n"
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
expect(lines).to eq ["CHGen inc.c", "CC program.o", "CC inc.o", "LD program#{env["PROGSUFFIX"]}"]
expect(File.exists?("inc.c")).to be_truthy
expect(File.exists?("inc.h")).to be_truthy
expect(`./program`).to eq "The value is 42\n"
File.open("inc.c", "w") {|fh| fh.puts "int THE_VALUE = 33;"}
env.process
expect(lines).to eq ["CHGen inc.c"]
expect(`./program`).to eq "The value is 42\n"
end
it 'allows cloning Environment objects' do
lines = test_dir('clone_env')
lines.should == [
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
expect(lines).to eq [
%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["PROGSUFFIX"]} debug/program.o},
%q{gcc -c -o release/program.o -MMD -MF release/program.mf '-DSTRING="Release Version"' -O2 src/program.c},
%q{gcc -o program-release release/program.o},
%Q{gcc -o program-release#{debug["PROGSUFFIX"]} release/program.o},
]
end
it 'allows cloning all attributes of an Environment object' do
test_dir('clone_env')
env1 = Rscons::Environment.new(echo: :command) do |env|
env.build_dir('src', 'build')
env['CFLAGS'] = '-O2'
env.add_build_hook do |build_op|
build_op[:vars]['CPPFLAGS'] = '-DSTRING="Hello"'
end
env.Program('program', Dir['src/*.c'])
end
env2 = env1.clone(clone: :all) do |env|
env.Program('program2', Dir['src/*.c'])
end
expect(lines).to eq [
%q{gcc -c -o build/program.o -MMD -MF build/program.mf -DSTRING="Hello" -O2 src/program.c},
%Q{gcc -o program#{env1["PROGSUFFIX"]} build/program.o},
%Q{gcc -o program2#{env2["PROGSUFFIX"]} build/program.o},
]
end
it 'builds a C++ program with one source file' do
test_dir('simple_cc')
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['*.cc'])
end
expect(File.exists?('simple.o')).to be_truthy
expect(`./simple`).to eq "This is a simple C++ program\n"
end
it 'allows overriding construction variables for individual builder calls' do
lines = test_dir('two_sources')
lines.should == [
test_dir('two_sources')
env = Rscons::Environment.new(echo: :command) do |env|
env.Object("one.o", "one.c", 'CPPFLAGS' => ['-DONE'])
env.Program('two_sources', ['one.o', 'two.c'])
end
expect(lines).to eq [
'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',
"gcc -o two_sources#{env["PROGSUFFIX"]} one.o two.o",
]
File.exists?('two_sources').should be_true
`./two_sources`.should == "This is a C program with two sources.\n"
expect(File.exists?("two_sources#{env["PROGSUFFIX"]}")).to be_truthy
expect(`./two_sources`).to eq "This is a C program with two sources.\n"
end
it 'builds a static library archive' do
lines = test_dir('library')
lines.should == [
test_dir('library')
env = 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
expect(lines).to eq [
'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',
"gcc -o library#{env["PROGSUFFIX"]} lib.a three.o",
]
File.exists?('library').should be_true
`ar t lib.a`.should == "one.o\ntwo.o\n"
expect(File.exists?("library#{env["PROGSUFFIX"]}")).to be_truthy
expect(`ar t lib.a`).to eq "one.o\ntwo.o\n"
end
it 'supports build hooks to override construction variables' do
test_dir("build_dir")
env = 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.exe', Dir['src/**/*.c'])
end
expect(`./build_hook.exe`).to eq "Hello from two()\n"
expect(lines).to match_array [
'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.exe build_one/one.o build_two/two.o',
]
end
it 'rebuilds when user-specified dependencies change' do
test_dir('simple')
env = Rscons::Environment.new do |env|
env.Program('simple.exe', Dir['*.c']).depends("file.ld")
File.open("file.ld", "w") do |fh|
fh.puts("foo")
end
end
expect(lines).to eq ["CC simple.o", "LD simple.exe"]
expect(File.exists?('simple.o')).to be_truthy
expect(`./simple.exe`).to eq "This is a simple C program\n"
e2 = Rscons::Environment.new do |env|
program = env.Program('simple.exe', Dir['*.c'])
env.depends(program, "file.ld")
File.open("file.ld", "w") do |fh|
fh.puts("bar")
end
end
expect(lines).to eq ["LD simple.exe"]
e3 = Rscons::Environment.new do |env|
env.Program('simple.exe', Dir['*.c'])
File.unlink("file.ld")
end
expect(lines).to eq ["LD simple.exe"]
Rscons::Environment.new do |env|
env.Program('simple.exe', Dir['*.c'])
end
expect(lines).to eq []
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.exe", Dir["*.d"])
end
expect(lines).to eq [
"gdc -c -o main.o main.d",
"gdc -o hello-d.exe main.o",
]
expect(`./hello-d.exe`.rstrip).to eq "Hello from D!"
end
end
it "supports disassembling object files" do
test_dir("simple")
Rscons::Environment.new do |env|
env.Object("simple.o", "simple.c")
env.Disassemble("simple.txt", "simple.o")
end
expect(File.exists?("simple.txt")).to be_truthy
expect(File.read("simple.txt")).to match /Disassembly of section .text:/
end
it "supports preprocessing C sources" do
test_dir("simple")
Rscons::Environment.new do |env|
env.Preprocess("simplepp.c", "simple.c")
env.Program("simple", "simplepp.c")
end
expect(File.read("simplepp.c")).to match /# \d+ "simple.c"/
expect(`./simple`).to eq "This is a simple C program\n"
end
it "supports preprocessing C++ sources" do
test_dir("simple_cc")
Rscons::Environment.new do |env|
env.Preprocess("simplepp.cc", "simple.cc")
env.Program("simple", "simplepp.cc")
end
expect(File.read("simplepp.cc")).to match /# \d+ "simple.cc"/
expect(`./simple`).to eq "This is a simple C++ program\n"
end
it "supports invoking builders with no sources and a build_root defined" do
class TestBuilder < Rscons::Builder
def run(target, sources, cache, env, vars)
target
end
end
test_dir("simple")
Rscons::Environment.new do |env|
env.build_root = "build"
env.add_builder(TestBuilder.new)
env.TestBuilder("file")
end
end
it "expands construction variables in builder target and sources before invoking the builder" do
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 678
EOF
end
target
end
end
env = Rscons::Environment.new do |env|
env["hdr"] = "inc.h"
env["src"] = "program.c"
env.add_builder(MySource.new)
env.MySource('${hdr}')
env.Program('program', "${src}")
end
expect(lines).to eq ["CC program.o", "LD program#{env["PROGSUFFIX"]}"]
expect(File.exists?('inc.h')).to be_truthy
expect(`./program`).to eq "The value is 678\n"
end
it "supports lambdas as construction variable values" do
env = Rscons::Environment.new do |env|
env["prefix"] = "-H"
env["suffix"] = "xyz"
env[:cfg] = {val: 44}
env["computed"] = lambda do |args|
"#{args[:env]['prefix']}#{args[:env][:cfg][:val]}#{args[:env]['suffix']}"
end
env["lambda_recurse"] = lambda do |args|
"${prefix}ello"
end
end
e2 = env.clone
e2[:cfg][:val] = 38
expect(env.expand_varref("${computed}")).to eq("-H44xyz")
expect(e2.expand_varref("${computed}")).to eq("-H38xyz")
expect(env.expand_varref("${lambda_recurse}")).to eq("-Hello")
end
end
+28
View File
@@ -0,0 +1,28 @@
module Rscons
module Builders
describe CFile do
let(:env) {Environment.new}
subject {CFile.new}
it "invokes bison to create a .c file from a .y file" do
expect(subject).to receive(:standard_build).with("YACC parser.c", "parser.c", ["bison", "-d", "-o", "parser.c", "parser.y"], ["parser.y"], env, :cache)
subject.run("parser.c", ["parser.y"], :cache, env, {})
end
it "invokes a custom lexer to create a .cc file from a .ll file" do
env["LEX"] = "custom_lex"
expect(subject).to receive(:standard_build).with("LEX lexer.cc", "lexer.cc", ["custom_lex", "-o", "lexer.cc", "parser.ll"], ["parser.ll"], env, :cache)
subject.run("lexer.cc", ["parser.ll"], :cache, env, {})
end
it "supports overriding construction variables" do
expect(subject).to receive(:standard_build).with("LEX lexer.c", "lexer.c", ["hi", "parser.l"], ["parser.l"], env, :cache)
subject.run("lexer.c", ["parser.l"], :cache, env, "LEX_CMD" => ["hi", "${_SOURCES}"])
end
it "raises an error when an unknown source file is specified" do
expect {subject.run("file.c", ["foo.bar"], :cache, env, {})}.to raise_error /Unknown source file .foo.bar. for CFile builder/
end
end
end
end
+17
View File
@@ -0,0 +1,17 @@
module Rscons
module Builders
describe Disassemble do
let(:env) {Environment.new}
subject {Disassemble.new}
it "supports overriding DISASM_CMD construction variable" do
cache = "cache"
allow(cache).to receive(:up_to_date?) { false }
allow(cache).to receive(:mkdir_p) { }
allow(cache).to receive(:register_build) { }
expect(env).to receive(:execute).with("Disassemble a_file.txt", ["my_disasm", "a_file.exe"], anything).and_return(true)
subject.run("a_file.txt", ["a_file.exe"], cache, env, "DISASM_CMD" => ["my_disasm", "${_SOURCES}"])
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
module Rscons
module Builders
describe Library do
let(:env) {Environment.new}
subject {Library.new}
it "supports overriding AR construction variable" do
expect(subject).to receive(:standard_build).with("AR prog.a", "prog.a", ["sp-ar", "rcs", "prog.a", "prog.o"], ["prog.o"], env, :cache)
subject.run("prog.a", ["prog.o"], :cache, env, "AR" => "sp-ar")
end
it "supports overriding ARCMD construction variable" do
expect(subject).to receive(:standard_build).with("AR prog.a", "prog.a", ["special", "AR!", "prog.o"], ["prog.o"], env, :cache)
subject.run("prog.a", ["prog.o"], :cache, env, "ARCMD" => ["special", "AR!", "${_SOURCES}"])
end
end
end
end
+23
View File
@@ -0,0 +1,23 @@
module Rscons
module Builders
describe Object do
let(:env) {Environment.new}
subject {Object.new}
it "supports overriding CCCMD construction variable" do
cache = "cache"
allow(cache).to receive(:up_to_date?) { false }
allow(cache).to receive(:mkdir_p) { }
allow(cache).to receive(:register_build) { }
allow(FileUtils).to receive(:rm_f) { }
allow(File).to receive(:exists?) { false }
expect(env).to receive(:execute).with("CC mod.o", ["llc", "mod.c"]).and_return(true)
subject.run("mod.o", ["mod.c"], cache, env, "CCCMD" => ["llc", "${_SOURCES}"])
end
it "raises an error when given a source file with an unknown suffix" do
expect { subject.run("mod.o", ["mod.xyz"], :cache, env, {}) }.to raise_error /unknown input file type: "mod.xyz"/
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
module Rscons
module Builders
describe Preprocess do
let(:env) {Environment.new}
subject {Preprocess.new}
it "supports overriding CC construction variable" do
expect(subject).to receive(:standard_build).with("Preprocess module.pp", "module.pp", ["my_cpp", "-E", "-o", "module.pp", "module.c"], ["module.c"], env, :cache)
subject.run("module.pp", ["module.c"], :cache, env, "CC" => "my_cpp")
end
it "supports overriding CPP_CMD construction variable" do
expect(subject).to receive(:standard_build).with("Preprocess module.pp", "module.pp", ["my_cpp", "module.c"], ["module.c"], env, :cache)
subject.run("module.pp", ["module.c"], :cache, env, "CPP_CMD" => ["my_cpp", "${_SOURCES}"])
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
module Rscons
module Builders
describe Program do
let(:env) {Environment.new}
subject {Program.new}
it "supports overriding CC construction variable" do
expect(subject).to receive(:standard_build).with("LD prog", "prog", ["sp-c++", "-o", "prog", "prog.o"], ["prog.o"], env, :cache)
subject.run("prog", ["prog.o"], :cache, env, "CC" => "sp-c++")
end
it "supports overriding LDCMD construction variable" do
expect(subject).to receive(:standard_build).with("LD prog.exe", "prog.exe", ["special", "LD!", "prog.o"], ["prog.o"], env, :cache)
subject.run("prog.exe", ["prog.o"], :cache, env, "LDCMD" => ["special", "LD!", "${_SOURCES}"])
end
end
end
end
+271
View File
@@ -0,0 +1,271 @@
module Rscons
describe Cache do
before do
allow(File).to receive(:read) { nil }
end
def build_from(cache)
allow(JSON).to receive(:load) do
cache
end
Cache.instance.tap do |cache|
cache.send(:initialize!)
end
end
describe "#initialize" do
context "when corrupt" do
it "prints a warning and defaults to an empty hash" do
expect(JSON).to receive(:load).and_return("string")
expect($stderr).to receive(:puts).with(/Warning:.*was.corrupt/)
c = Cache.instance
c.send(:initialize!)
expect(c.instance_variable_get(:@cache).is_a?(Hash)).to be_truthy
end
end
end
describe "#clear" do
it "removes the cache file" do
expect(FileUtils).to receive(:rm_f).with(Cache::CACHE_FILE)
allow(JSON).to receive(:load) {{}}
Cache.instance.clear
end
end
describe "#write" do
it "fills in 'version' and write to file" do
cache = {}
fh = $stdout
expect(fh).to receive(:puts)
expect(File).to receive(:open).and_yield(fh)
build_from(cache).write
expect(cache["version"]).to eq Rscons::VERSION
end
end
describe "#up_to_date?" do
empty_env = "env"
before do
allow(empty_env).to receive(:get_user_deps) { nil }
end
it "returns false when target file does not exist" do
expect(File).to receive(:exists?).with("target").and_return(false)
expect(build_from({}).up_to_date?("target", "command", [], empty_env)).to be_falsey
end
it "returns false when target is not registered in the cache" do
expect(File).to receive(:exists?).with("target").and_return(true)
expect(build_from({}).up_to_date?("target", "command", [], empty_env)).to be_falsey
end
it "returns false when the target's checksum does not match" do
_cache = {"targets" => {"target" => {"checksum" => "abc"}}}
cache = build_from(_cache)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("def")
expect(cache.up_to_date?("target", "command", [], empty_env)).to be_falsey
end
it "returns false when the build command has changed" do
_cache = {"targets" => {"target" => {"checksum" => "abc", "command" => Digest::MD5.hexdigest("old command".inspect)}}}
cache = build_from(_cache)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache.up_to_date?("target", "command", [], empty_env)).to be_falsey
end
it "returns false when there is a new dependency" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"deps" => [{"fname" => "dep.1"}]}}}
cache = build_from(_cache)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env)).to be_falsey
end
it "returns false when a dependency's checksum has changed" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"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)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache).to receive(:calculate_checksum).with("dep.1").and_return("dep.1.chk")
expect(cache).to receive(:calculate_checksum).with("dep.2").and_return("dep.2.changed")
expect(cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env)).to be_falsey
end
it "returns false with strict_deps=true when cache has an extra dependency" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"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)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env, strict_deps: true)).to be_falsey
end
it "returns false when there is a new user dependency" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"deps" => [{"fname" => "dep.1"}],
"user_deps" => []}}}
cache = build_from(_cache)
env = "env"
expect(env).to receive(:get_user_deps).with("target").and_return(["file.ld"])
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache.up_to_date?("target", "command", ["dep.1"], env)).to be_falsey
end
it "returns false when a user dependency checksum has changed" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"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"
expect(env).to receive(:get_user_deps).with("target").and_return(["user.dep"])
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache).to receive(:calculate_checksum).with("dep.1").and_return("dep.1.chk")
expect(cache).to receive(:calculate_checksum).with("dep.2").and_return("dep.2.chk")
expect(cache).to receive(:calculate_checksum).with("extra.dep").and_return("extra.dep.chk")
expect(cache).to receive(:calculate_checksum).with("user.dep").and_return("INCORRECT")
expect(cache.up_to_date?("target", "command", ["dep.1", "dep.2"], env)).to be_falsey
end
it "returns true when no condition for false is met" do
_cache = {"targets" => {"target" => {"checksum" => "abc",
"command" => Digest::MD5.hexdigest("command".inspect),
"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)
expect(File).to receive(:exists?).with("target").and_return(true)
expect(cache).to receive(:calculate_checksum).with("target").and_return("abc")
expect(cache).to receive(:calculate_checksum).with("dep.1").and_return("dep.1.chk")
expect(cache).to receive(:calculate_checksum).with("dep.2").and_return("dep.2.chk")
expect(cache).to receive(:calculate_checksum).with("extra.dep").and_return("extra.dep.chk")
expect(cache.up_to_date?("target", "command", ["dep.1", "dep.2"], empty_env)).to be_truthy
end
end
describe "#register_build" do
it "stores the given information in the cache" do
_cache = {}
cache = build_from(_cache)
env = "env"
expect(env).to receive(:get_user_deps).with("the target").and_return(["user.dep"])
expect(cache).to receive(:calculate_checksum).with("the target").and_return("the checksum")
expect(cache).to receive(:calculate_checksum).with("dep 1").and_return("dep 1 checksum")
expect(cache).to receive(:calculate_checksum).with("dep 2").and_return("dep 2 checksum")
expect(cache).to 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"]
expect(cached_target).to_not be_nil
expect(cached_target["command"]).to eq Digest::MD5.hexdigest("the command".inspect)
expect(cached_target["checksum"]).to eq "the checksum"
expect(cached_target["deps"]).to eq [
{"fname" => "dep 1", "checksum" => "dep 1 checksum"},
{"fname" => "dep 2", "checksum" => "dep 2 checksum"},
]
expect(cached_target["user_deps"]).to eq [
{"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" => {}}}
expect(build_from(cache).targets).to eq ["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)
expect(File).to receive(:exists?).with("one").and_return(true)
expect(File).to receive(:exists?).with("one/two").and_return(false)
expect(FileUtils).to receive(:mkdir).with("one/two")
expect(File).to receive(:exists?).with("one/two/three").and_return(false)
expect(FileUtils).to receive(:mkdir).with("one/two/three")
expect(File).to receive(:exists?).with("one").and_return(true)
expect(File).to receive(:exists?).with("one/two").and_return(true)
expect(File).to receive(:exists?).with("one/two/four").and_return(false)
expect(FileUtils).to receive(:mkdir).with("one/two/four")
cache.mkdir_p("one/two/three")
cache.mkdir_p("one\\two\\four")
expect(cache.directories).to eq ["one/two", "one/two/three", "one/two/four"]
end
it "handles absolute paths" do
_cache = {}
cache = build_from(_cache)
expect(File).to receive(:exists?).with("/one").and_return(true)
expect(File).to receive(:exists?).with("/one/two").and_return(false)
expect(FileUtils).to receive(:mkdir).with("/one/two")
cache.mkdir_p("/one/two")
expect(cache.directories).to eq ["/one/two"]
end
end
describe "#directories" do
it "returns a list of directories that are cached" do
_cache = {"directories" => {"dir1" => true, "dir2" => true}}
expect(build_from(_cache).directories).to eq ["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"})
expect(cache).to_not receive(:calculate_checksum)
expect(cache.send(:lookup_checksum, "f1")).to eq "f1.chk"
end
it "calls calculate_checksum when the checksum is not cached" do
cache = build_from({})
expect(cache).to receive(:calculate_checksum).with("f1").and_return("ck")
expect(cache.send(:lookup_checksum, "f1")).to eq "ck"
end
end
describe "#calculate_checksum" do
it "calculates the MD5 of the file contents" do
contents = "contents"
expect(File).to receive(:read).with("fname", mode: "rb").and_return(contents)
expect(Digest::MD5).to receive(:hexdigest).with(contents).and_return("the_checksum")
expect(build_from({}).send(:calculate_checksum, "fname")).to eq "the_checksum"
end
end
end
end
+438 -7
View File
@@ -1,22 +1,453 @@
module Rscons
describe Environment do
describe '.parse_makefile_deps' do
describe "#initialize" do
it "adds the default builders when they are not excluded" do
env = Environment.new
expect(env.builders.size).to be > 0
expect(env.builders.map {|name, builder| builder.is_a?(Builder)}.all?).to be_truthy
expect(env.builders.find {|name, builder| name == "Object"}).to_not be_nil
expect(env.builders.find {|name, builder| name == "Program"}).to_not be_nil
expect(env.builders.find {|name, builder| name == "Library"}).to_not be_nil
end
it "excludes the default builders with exclude_builders: :all" do
env = Environment.new(exclude_builders: true)
expect(env.builders.size).to eq 0
end
context "when a block is given" do
it "yields self and invokes #process()" do
env = Environment.new do |env|
expect(env).to receive(:process)
end
end
end
end
describe "#clone" do
it 'creates unique copies of each construction variable' do
env = Environment.new
env["CPPPATH"] << "path1"
env2 = env.clone
env2["CPPPATH"] << "path2"
expect(env["CPPPATH"]).to eq ["path1"]
expect(env2["CPPPATH"]).to eq ["path1", "path2"]
end
it "supports nil, false, true, String, Symbol, Array, Hash, and Integer variables" do
env = Environment.new
env["nil"] = nil
env["false"] = false
env["true"] = true
env["String"] = "String"
env["Symbol"] = :Symbol
env["Array"] = ["a", "b"]
env["Hash"] = {"a" => "b"}
env["Integer"] = 1234
env2 = env.clone
expect(env2["nil"]).to be_nil
expect(env2["false"].object_id).to eq(false.object_id)
expect(env2["true"].object_id).to eq(true.object_id)
expect(env2["String"]).to eq("String")
expect(env2["Symbol"]).to eq(:Symbol)
expect(env2["Array"]).to eq(["a", "b"])
expect(env2["Hash"]).to eq({"a" => "b"})
expect(env2["Integer"]).to eq(1234)
end
context "when a block is given" do
it "yields self and invokes #process()" do
env = Environment.new
env.clone do |env2|
expect(env2).to 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: true)
expect(env.builders.keys).to eq []
env.add_builder(Rscons::Builders::Object.new)
expect(env.builders.keys).to eq ["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
expect(env.get_build_fname("src/dir/file.c", ".o")).to eq "src/dir/file.o"
expect(env.get_build_fname("src\\dir\\other.d", ".a")).to eq "src/dir/other.a"
expect(env.get_build_fname("source.cc", ".o")).to eq "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"
expect(env.get_build_fname("src/dir/file.c", ".o")).to eq "build/proj/src/dir/file.o"
expect(env.get_build_fname("/some/lib.c", ".a")).to eq "/some/lib.a"
expect(env.get_build_fname("C:\\abspath\\mod.cc", ".o")).to eq "C:/abspath/mod.o"
expect(env.get_build_fname("build\\proj\\generated.c", ".o")).to eq "build/proj/generated.o"
expect(env.get_build_fname("build/proj.XX", ".yy")).to eq "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')
expect(env.get_build_fname("src/input.cc", ".o")).to eq "bld/input.o"
expect(env.get_build_fname("libs/lib1/some/file.c", ".o")).to eq "build/libs/lib1/some/file.o"
expect(env.get_build_fname("libs/otherlib/otherlib.cc", ".o")).to eq "build/libs/otherlib/otherlib.o"
expect(env.get_build_fname("other_directory/o.d", ".a")).to eq "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"
expect(env.get_build_fname("src/input.cc", ".o")).to eq "bld/input.o"
expect(env.get_build_fname("libs/lib1/some/file.c", ".o")).to eq "build/libs/lib1/some/file.o"
expect(env.get_build_fname("libs/otherlib/otherlib.cc", ".o")).to eq "build/libs/otherlib/otherlib.o"
expect(env.get_build_fname("other_directory/o.d", ".a")).to eq "bldit/other_directory/o.a"
expect(env.get_build_fname("bldit/some/mod.d", ".a")).to eq "bldit/some/mod.a"
end
end
end
end
describe "#[]" do
it "allows reading construction variables" do
env = Environment.new
env["CFLAGS"] = ["-g", "-Wall"]
expect(env["CFLAGS"]).to eq ["-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"]
expect(env["CFLAGS"]).to eq ["-Wall", "-O3"]
env["other_var"] = "val33"
expect(env["other_var"]).to eq "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"])
expect(env["CFLAGS"]).to eq ["-Wall"]
expect(env["CPPPATH"]).to eq ["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"
expect(Cache).to receive(:instance).and_return(cache)
expect(cache).to receive(:clear_checksum_cache!)
expect(env).to receive(:run_builder).with(anything, "a.out", ["main.c"], cache, {}).and_return(true)
expect(cache).to 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"
expect(Cache).to receive(:instance).and_return(cache)
expect(cache).to receive(:clear_checksum_cache!)
expect(env).to receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return("main.o")
expect(env).to receive(:run_builder).with(anything, "a.out", ["main.o"], cache, {}).and_return("a.out")
expect(cache).to 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"
expect(Cache).to receive(:instance).and_return(cache)
expect(cache).to receive(:clear_checksum_cache!)
expect(env).to receive(:run_builder).with(anything, "main.o", ["other.cc"], cache, {}).and_return(false)
expect(cache).to receive(:write)
expect { env.process }.to raise_error BuildError, /Failed.to.build.main.o/
end
it "writes the cache when the Builder raises an exception" do
env = Environment.new
env.Object("module.o", "module.c")
cache = "cache"
expect(Cache).to receive(:instance).and_return(cache)
expect(cache).to receive(:clear_checksum_cache!)
allow(env).to receive(:run_builder) do |builder, target, sources, cache, vars|
raise "Ruby exception thrown by builder"
end
expect(cache).to receive(:write)
expect { env.process }.to raise_error RuntimeError, /Ruby exception thrown by builder/
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")
expect(cmd).to eq ["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["foo"] = {}
expect(env.expand_varref(["-p${path}", "${flags}"])).to eq ["-pdir1", "-pdir2", "-x", "-y", "-z"]
expect(env.expand_varref("foo")).to eq "foo"
expect {env.expand_varref("${foo}")}.to raise_error /Unknown.varref.type/
expect(env.expand_varref("${specialflag}")).to eq "-z"
expect(env.expand_varref("${path}")).to eq ["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)
expect(env).to receive(:puts).with("short desc")
expect(env).to receive(:system).with(*Rscons.command_executer, "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)
expect(env).to receive(:puts).with("short desc")
expect(env).to receive(:system).with(*Rscons.command_executer, "a", "command").and_return(false)
expect($stdout).to receive(:write).with("Failed command was: ")
expect(env).to 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)
expect(env).to receive(:puts).with("a command '--arg=val with spaces'")
expect(env).to receive(:system).with({modified: :environment}, *Rscons.command_executer, "a", "command", "--arg=val with spaces", {opt: :val}).and_return(false)
env.execute("short desc", ["a", "command", "--arg=val with spaces"], env: {modified: :environment}, options: {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
expect {env.foobar}.to raise_error /undefined method .foobar./
end
it "records the target when the target method is a known builder" do
env = Environment.new
expect(env.instance_variable_get(:@targets)).to eq({})
env.Object("target.o", ["src1.c", "src2.c"], var: "val")
target = env.instance_variable_get(:@targets)["target.o"]
expect(target).to_not be_nil
expect(target[:builder].is_a?(Builder)).to be_truthy
expect(target[:sources]).to eq ["src1.c", "src2.c"]
expect(target[:vars]).to eq({var: "val"})
expect(target[:args]).to eq []
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")
expect(env.instance_variable_get(:@user_deps)).to eq({"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")
expect(env.instance_variable_get(:@user_deps)).to eq({"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)
expect(env.builders["Object"]).to receive(:run).with("mod.o", ["mod.c"], cache, env, anything).and_return("mod.o")
expect(env.builders["ABuilder"]).to receive(:run).with("mod2.ab_out", ["mod2.ab_in"], cache, env, anything).and_return("mod2.ab_out")
expect(env.build_sources(["precompiled.o", "mod.c", "mod2.ab_in"], [".o", ".ab_out"], cache, {})).to eq ["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
allow(env.builders["Object"]).to receive(:run) do |target, sources, cache, env, vars|
expect(vars["CFLAGS"]).to eq []
end
env.run_builder(env.builders["Object"], "build/normal/module.o", ["src/normal/module.c"], "cache", {})
allow(env.builders["Object"]).to receive(:run) do |target, sources, cache, env, vars|
expect(vars["CFLAGS"]).to eq ["-O3", "-DSPECIAL"]
end
env.run_builder(env.builders["Object"], "build/special/module.o", ["src/special/module.c"], "cache", {})
end
end
describe "#shell" do
it "executes the given shell command and returns the results" do
env = Environment.new
expect(env.shell("echo hello").strip).to eq("hello")
end
it "determines shell flag to be /c when SHELL is specified as 'cmd'" do
env = Environment.new
env["SHELL"] = "cmd"
expect(IO).to receive(:popen).with(["cmd", "/c", "my_cmd"])
env.shell("my_cmd")
end
it "determines shell flag to be -c when SHELL is specified as something else" do
env = Environment.new
env["SHELL"] = "my_shell"
expect(IO).to receive(:popen).with(["my_shell", "-c", "my_cmd"])
env.shell("my_cmd")
end
end
describe "#parse_flags" do
it "executes the shell command and parses the returned flags when the input argument begins with !" do
env = Environment.new
env["CFLAGS"] = ["-g"]
expect(env).to receive(:shell).with("my_command").and_return(%[-arch my_arch -Done=two -include ii -isysroot sr -Iincdir -Llibdir -lmy_lib -mno-cygwin -mwindows -pthread -std=c99 -Wa,'asm,args 1 2' -Wl,linker,"args 1 2" -Wp,cpp,args,1,2 -arbitrary +other_arbitrary some_lib /a/b/c/lib])
rv = env.parse_flags("!my_command")
expect(rv).to eq({
"CCFLAGS" => %w[-arch my_arch -include ii -isysroot sr -mno-cygwin -pthread -arbitrary +other_arbitrary],
"LDFLAGS" => %w[-arch my_arch -isysroot sr -mno-cygwin -mwindows -pthread] + ["linker", "args 1 2"] + %w[+other_arbitrary],
"CPPPATH" => %w[incdir],
"LIBS" => %w[my_lib some_lib /a/b/c/lib],
"LIBPATH" => %w[libdir],
"CPPDEFINES" => %w[one=two],
"CFLAGS" => %w[-std=c99],
"ASFLAGS" => ["asm", "args 1 2"],
"CPPFLAGS" => %w[cpp args 1 2],
})
expect(env["CFLAGS"]).to eq(["-g"])
expect(env["ASFLAGS"]).to eq([])
env.merge_flags(rv)
expect(env["CFLAGS"]).to eq(["-g", "-std=c99"])
expect(env["ASFLAGS"]).to eq(["asm", "args 1 2"])
end
end
describe "#parse_flags!" do
it "parses the given build flags and merges them into the Environment" do
env = Environment.new
env["CFLAGS"] = ["-g"]
rv = env.parse_flags!("-I incdir -D my_define -L /a/libdir -l /some/lib")
expect(rv).to eq({
"CPPPATH" => %w[incdir],
"LIBS" => %w[/some/lib],
"LIBPATH" => %w[/a/libdir],
"CPPDEFINES" => %w[my_define],
})
expect(env["CPPPATH"]).to eq(%w[incdir])
expect(env["LIBS"]).to eq(%w[/some/lib])
expect(env["LIBPATH"]).to eq(%w[/a/libdir])
expect(env["CPPDEFINES"]).to eq(%w[my_define])
end
end
describe "#merge_flags" do
it "appends array contents and replaces other variable values" do
env = Environment.new
env["CPPPATH"] = ["incdir"]
env["CSUFFIX"] = ".x"
env.merge_flags("CPPPATH" => ["a"], "CSUFFIX" => ".c")
expect(env["CPPPATH"]).to eq(%w[incdir a])
expect(env["CSUFFIX"]).to eq(".c")
end
end
describe ".parse_makefile_deps" do
it 'handles dependencies on one line' do
File.should_receive(:read).with('makefile').and_return(<<EOS)
expect(File).to receive(:read).with('makefile').and_return(<<EOS)
module.o: source.cc
EOS
env = Environment.new
env.parse_makefile_deps('makefile', 'module.o').should == ['source.cc']
expect(Environment.parse_makefile_deps('makefile', 'module.o')).to eq ['source.cc']
end
it 'handles dependencies split across many lines' do
File.should_receive(:read).with('makefile').and_return(<<EOS)
expect(File).to receive(:read).with('makefile').and_return(<<EOS)
module.o: module.c \\
module.h \\
other.h
EOS
env = Environment.new
env.parse_makefile_deps('makefile', 'module.o').should == [
expect(Environment.parse_makefile_deps('makefile', 'module.o')).to eq [
'module.c', 'module.h', 'other.h']
end
end
+172
View File
@@ -0,0 +1,172 @@
module Rscons
describe VarSet do
describe '#initialize' do
it "initializes variables from a Hash" do
v = VarSet.new({"one" => 1, "two" => :two})
expect(v["one"]).to eq(1)
expect(v["two"]).to eq(:two)
end
it "initializes variables from another VarSet" do
v = VarSet.new({"one" => 1})
v2 = VarSet.new(v)
expect(v2["one"]).to eq 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
expect(v["array"]).to eq([1, 2, 3, 4])
expect(v2["array"]).to eq([1, 2, 3])
end
end
describe "#[]" do
it "allows accessing a variable with its verbatim value if type is not specified" do
v = VarSet.new({"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [3, 6], "qux" => {a: :b}})
expect(v["fuz"]).to eq("a string")
expect(v["foo"]).to eq(42)
expect(v["bar"]).to eq(:baz)
expect(v["qax"]).to eq([3, 6])
expect(v["qux"]).to eq({a: :b})
end
end
describe "#[]=" do
it "allows assigning to variables" do
v = VarSet.new("CFLAGS" => ["-Wall", "-O3"])
v["CPPPATH"] = ["one", "two"]
expect(v["CFLAGS"]).to eq(["-Wall", "-O3"])
expect(v["CPPPATH"]).to eq(["one", "two"])
end
end
describe "#include?" do
it "returns whether the variable is in the VarSet" do
v = VarSet.new("CFLAGS" => [], :foo => :bar)
expect(v.include?("CFLAGS")).to be_truthy
expect(v.include?(:CFLAGS)).to be_falsey
expect(v.include?(:foo)).to be_truthy
expect(v.include?("foo")).to be_falsey
expect(v.include?("bar")).to be_falsey
v2 = v.clone
v2.append("bar" => [])
expect(v2.include?("CFLAGS")).to be_truthy
expect(v2.include?(:CFLAGS)).to be_falsey
expect(v2.include?(:foo)).to be_truthy
expect(v2.include?("foo")).to be_falsey
expect(v2.include?("bar")).to be_truthy
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"])
expect(v["LDFLAGS"]).to eq("-lgcc")
expect(v["LIBS"]).to eq("gcc")
expect(v["LIBPATH"]).to eq(["mylibs"])
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)
expect(v["CFLAGS"]).to eq(["-O0"])
expect(v["CPPPATH"]).to eq(["different_dir"])
end
it "does not pick up subsequent variable changes from a given VarSet" do
v = VarSet.new("dirs" => ["a"])
v2 = VarSet.new
v2.append(v)
v["dirs"] << "b"
expect(v["dirs"]).to eq(["a", "b"])
expect(v2["dirs"]).to eq(["a"])
end
end
describe '#merge' do
it "returns a new VarSet merged with the given Hash" do
v = VarSet.new("foo" => "yoda")
v2 = v.merge("baz" => "qux")
expect(v["foo"]).to eq("yoda")
expect(v2["foo"]).to eq("yoda")
expect(v2["baz"]).to eq("qux")
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"
expect(v["foo"]).to eq ["a", "b"]
expect(v["bar"]).to eq 42
expect(v2["foo"]).to eq ["a", "b", "c"]
expect(v2["bar"]).to eq 33
end
it "does not pick up subsequent variable changes from a given VarSet" do
v = VarSet.new("var" => ["a", "b"], "var2" => ["1", "2"])
v["var2"] << "3"
v2 = v.clone
v["var"] << "c"
expect(v["var"]).to eq(["a", "b", "c"])
expect(v["var2"]).to eq(["1", "2", "3"])
expect(v2["var"]).to eq(["a", "b"])
expect(v2["var2"]).to eq(["1", "2", "3"])
end
end
describe '#expand_varref' do
v = VarSet.new("CFLAGS" => ["-Wall", "-O2"],
"CC" => "gcc",
"CPPPATH" => ["dir1", "dir2"],
"compiler" => "${CC}",
"cmd" => ["${CC}", "-c", "${CFLAGS}", "-I${CPPPATH}"],
"lambda" => lambda {|args| "#{args[:v]}--12"})
it "expands to the string itself if the string is not a variable reference" do
expect(v.expand_varref("CC", :lambda_args)).to eq("CC")
expect(v.expand_varref("CPPPATH", :lambda_args)).to eq("CPPPATH")
expect(v.expand_varref("str", :lambda_args)).to eq("str")
end
it "expands a single variable reference beginning with a '$'" do
expect(v.expand_varref("${CC}", :lambda_args)).to eq("gcc")
expect(v.expand_varref("${CPPPATH}", :lambda_args)).to eq(["dir1", "dir2"])
end
it "expands a single variable reference in ${arr} notation" do
expect(v.expand_varref("prefix${CFLAGS}suffix", :lambda_args)).to eq(["prefix-Wallsuffix", "prefix-O2suffix"])
expect(v.expand_varref(v["cmd"], :lambda_args)).to eq(["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"])
end
it "expands a variable reference recursively" do
expect(v.expand_varref("${compiler}", :lambda_args)).to eq("gcc")
expect(v.expand_varref("${cmd}", :lambda_args)).to eq(["gcc", "-c", "-Wall", "-O2", "-Idir1", "-Idir2"])
end
it "resolves multiple variable references in one element by enumerating all combinations" do
expect(v.expand_varref("cflag: ${CFLAGS}, cpppath: ${CPPPATH}, compiler: ${compiler}", :lambda_args)).to eq([
"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 "returns an empty string when a variable reference refers to a non-existent variable" do
expect(v.expand_varref("${not_here}", :lambda_args)).to eq("")
end
it "calls a lambda with the given lambda arguments" do
expect(v.expand_varref("${lambda}", [v: "fez"])).to eq("fez--12")
end
it "raises an error when given an invalid argument" do
expect { v.expand_varref({a: :b}, :lambda_args) }.to raise_error /Unknown varref type: Hash/
end
it "raises an error when an expanded variable is an unexpected type" do
expect(v).to receive(:[]).at_least(1).times.with("bad").and_return("bad_val")
expect(v).to receive(:expand_varref).with("bad_val", :lambda_args).and_return({a: :b})
expect(v).to receive(:expand_varref).and_call_original
expect { v.expand_varref("${bad}", :lambda_args) }.to raise_error /I do not know how to expand a variable reference to a Hash/
end
end
end
end
+115
View File
@@ -0,0 +1,115 @@
describe Rscons do
describe ".clean" do
it "removes all build targets and created directories" do
cache = "cache"
expect(Rscons::Cache).to receive(:instance).and_return(cache)
expect(cache).to receive(:targets).and_return(["build/a.out", "build/main.o"])
expect(FileUtils).to receive(:rm_f).with("build/a.out")
expect(FileUtils).to receive(:rm_f).with("build/main.o")
expect(cache).to receive(:directories).and_return(["build/one", "build/one/two", "build", "other"])
expect(File).to receive(:directory?).with("build/one/two").and_return(true)
expect(Dir).to receive(:entries).with("build/one/two").and_return([".", ".."])
expect(Dir).to receive(:rmdir).with("build/one/two")
expect(File).to receive(:directory?).with("build/one").and_return(true)
expect(Dir).to receive(:entries).with("build/one").and_return([".", ".."])
expect(Dir).to receive(:rmdir).with("build/one")
expect(File).to receive(:directory?).with("build").and_return(true)
expect(Dir).to receive(:entries).with("build").and_return([".", ".."])
expect(Dir).to receive(:rmdir).with("build")
expect(File).to receive(:directory?).with("other").and_return(true)
expect(Dir).to receive(:entries).with("other").and_return([".", "..", "other.file"])
expect(cache).to receive(:clear)
Rscons.clean
end
end
describe ".get_system_shell" do
before(:each) do
Rscons.class_variable_set(:@@shell, nil)
end
after(:each) do
Rscons.class_variable_set(:@@shell, nil)
end
it "uses the SHELL environment variable if it tests successfully" do
my_ENV = {"SHELL" => "my_shell"}
allow(ENV).to receive(:[]) {|*args| my_ENV[*args]}
io = StringIO.new("success\n")
expect(IO).to receive(:popen).with(["my_shell", "-c", "echo success"]).and_yield(io)
expect(Rscons.get_system_shell).to eq(["my_shell", "-c"])
end
it "uses sh -c on a mingw platform if it tests successfully" do
my_ENV = {"SHELL" => nil}
allow(ENV).to receive(:[]) {|*args| my_ENV[*args]}
io = StringIO.new("success\n")
expect(IO).to receive(:popen).with(["sh", "-c", "echo success"]).and_yield(io)
expect(Object).to receive(:const_get).with("RUBY_PLATFORM").and_return("x86-mingw")
expect(Rscons.get_system_shell).to eq(["sh", "-c"])
end
it "uses cmd /c on a mingw platform if sh -c does not test successfully" do
my_ENV = {"SHELL" => nil}
allow(ENV).to receive(:[]) {|*args| my_ENV[*args]}
io = StringIO.new("success\n")
expect(IO).to receive(:popen).with(["sh", "-c", "echo success"]).and_raise "ENOENT"
expect(Object).to receive(:const_get).with("RUBY_PLATFORM").and_return("x86-mingw")
expect(Rscons.get_system_shell).to eq(["cmd", "/c"])
end
it "uses sh -c on a non-mingw platform if SHELL is not specified" do
my_ENV = {"SHELL" => nil}
allow(ENV).to receive(:[]) {|*args| my_ENV[*args]}
expect(Object).to receive(:const_get).with("RUBY_PLATFORM").and_return("x86-linux")
expect(Rscons.get_system_shell).to eq(["sh", "-c"])
end
end
context "command executer" do
describe ".command_executer" do
before(:each) do
Rscons.class_variable_set(:@@command_executer, nil)
end
after(:each) do
Rscons.class_variable_set(:@@command_executer, nil)
end
it "returns ['env'] if mingw platform in MSYS and 'env' works" do
expect(Object).to receive(:const_get).and_return("x86-mingw")
expect(ENV).to receive(:keys).and_return(["MSYSCON"])
io = StringIO.new("success\n")
expect(IO).to receive(:popen).with(["env", "echo", "success"]).and_yield(io)
expect(Rscons.command_executer).to eq(["env"])
end
it "returns [] if mingw platform in MSYS and 'env' does not work" do
expect(Object).to receive(:const_get).and_return("x86-mingw")
expect(ENV).to receive(:keys).and_return(["MSYSCON"])
expect(IO).to receive(:popen).with(["env", "echo", "success"]).and_raise "ENOENT"
expect(Rscons.command_executer).to eq([])
end
it "returns [] if mingw platform not in MSYS" do
expect(Object).to receive(:const_get).and_return("x86-mingw")
expect(ENV).to receive(:keys).and_return(["COMSPEC"])
expect(Rscons.command_executer).to eq([])
end
it "returns [] if not mingw platform" do
expect(Object).to receive(:const_get).and_return("x86-linux")
expect(Rscons.command_executer).to eq([])
end
end
describe ".command_executer=" do
it "overrides the value of @@command_executer" do
Rscons.class_variable_set(:@@command_executer, ["env"])
Rscons.command_executer = []
expect(Rscons.class_variable_get(:@@command_executer)).to eq([])
end
end
end
end
+6
View File
@@ -1 +1,7 @@
require "simplecov"
SimpleCov.start do
add_filter "/spec/"
end
require "rscons"