Compare commits

...
12 Commits
9 changed files with 195 additions and 108 deletions

No files matched your search

+74 -11
View File
@@ -27,7 +27,7 @@ called from your Rakefile.
### Example: Building a C Program ### Example: Building a C Program
```ruby ```ruby
RScons::Environment.new do |env| Rscons::Environment.new do |env|
env["CFLAGS"] << "-Wall" env["CFLAGS"] << "-Wall"
env.Program("program", Dir["**/*.c"]) env.Program("program", Dir["**/*.c"])
end end
@@ -36,7 +36,7 @@ end
### Example: Building a D Program ### Example: Building a D Program
```ruby ```ruby
RScons::Environment.new do |env| Rscons::Environment.new do |env|
env["DFLAGS"] << "-Wall" env["DFLAGS"] << "-Wall"
env.Program("program", Dir["**/*.d"]) env.Program("program", Dir["**/*.d"])
end end
@@ -45,7 +45,7 @@ end
### Example: Cloning an Environment ### Example: Cloning an Environment
```ruby ```ruby
main_env = RScons::Environment.new do |env| main_env = Rscons::Environment.new do |env|
# Store object files from sources under "src" in "build/main" # Store object files from sources under "src" in "build/main"
env.build_dir("src", "build/main") env.build_dir("src", "build/main")
env["CFLAGS"] = ["-DSOME_DEFINE", "-O3"] env["CFLAGS"] = ["-DSOME_DEFINE", "-O3"]
@@ -64,6 +64,11 @@ end
### Example: Custom Builder ### 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 ```ruby
class GenerateFoo < Rscons::Builder class GenerateFoo < Rscons::Builder
def run(target, sources, cache, env, vars) def run(target, sources, cache, env, vars)
@@ -73,10 +78,12 @@ class GenerateFoo < Rscons::Builder
#define GENERATED 42 #define GENERATED 42
EOF EOF
end end
target
end end
end end
Rscons::Environment.new do |env| Rscons::Environment.new do |env|
env.add_builder(GenerateFoo.new)
env.GenerateFoo("foo.h", []) env.GenerateFoo("foo.h", [])
env.Program("a.out", Dir["*.c"]) env.Program("a.out", Dir["*.c"])
end end
@@ -93,16 +100,46 @@ class CmdBuilder < Rscons::Builder
system(cmd) system(cmd)
cache.register_build(target, cmd, sources, env) cache.register_build(target, cmd, sources, env)
end end
target
end end
end end
Rscons::Environment.new do |env| Rscons::Environment.new do |env|
env.add_builder(CmdBuilder.new)
env.CmdBuilder("foo.gen", "foo_gen.cfg") env.CmdBuilder("foo.gen", "foo_gen.cfg")
end 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() ### 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 ```ruby
class CmdBuilder < Rscons::Builder class CmdBuilder < Rscons::Builder
def run(target, sources, cache, env, vars) def run(target, sources, cache, env, vars)
@@ -112,6 +149,7 @@ class CmdBuilder < Rscons::Builder
end end
Rscons::Environment.new do |env| Rscons::Environment.new do |env|
env.add_builder(CmdBuilder.new)
env.CmdBuilder("foo.gen", "foo_gen.cfg") env.CmdBuilder("foo.gen", "foo_gen.cfg")
end end
``` ```
@@ -131,6 +169,19 @@ Rscons::Environment.new do |env|
end end
``` ```
Each build hook block will be invoked for every build operation, so the block
should test the target or sources if its action should only apply to some
subset of build targets or source files.
The `build_op` parameter to the build hook block is a Hash describing the
build operation with the following keys:
* `:builder` - `Builder` instance in use
* `:target` - `String` name of the target file
* `:sources` - `Array` of the source files
* `:vars` - `Rscons::VarSet` containing the construction variables to use
The build hook can overwrite entries in `build_op[:vars]` to alter the
construction variables in use for this specific build operation.
### Example: Creating a static library ### Example: Creating a static library
```ruby ```ruby
@@ -149,8 +200,8 @@ Rscons ships with a number of default builders:
* Object, which compiles source files to produce an object file * Object, which compiles source files to produce an object file
* Program, which links object files to produce an executable * Program, which links object files to produce an executable
If you want to create an Environment that does not contain any (or select) If you want to create an Environment that does not contain any builders,
builders, you can use the `exclude_builders` key to the Environment constructor. you can use the `exclude_builders` key to the Environment constructor.
### Managing Environments ### Managing Environments
@@ -158,7 +209,8 @@ An Rscons::Environment consists of:
* a collection of construction variables * a collection of construction variables
* a collection of builders * a collection of builders
* a mapping of build directories * 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 targets to build
* a collection of build hooks * a collection of build hooks
@@ -178,12 +230,23 @@ cloned_env["CPPPATH"] << "three"
`base_env["CPPPATH"]` will not include "three". `base_env["CPPPATH"]` will not include "three".
### Construction Variables ### Construction Variable Naming
The default construction variables that Rscons uses are named using uppercase * uppercase strings - the default construction variables that Rscons uses
strings. * lowercase symbols - Rscons options
Rscons options are lowercase symbols. * lowercase strings - reserved as user-defined construction variables
Lowercase strings are reserved as user-defined construction variables.
### API documentation
Documentation for the complete Rscons API can be found at
http://rubydoc.info/github/holtrop/rscons/frames.
## Release Notes
### v1.1.0
- Change Cache#up_to_date?() and #register_build() to accept a single target
file or an array of target file names
## Contributing ## Contributing
+21 -10
View File
@@ -1,15 +1,13 @@
require "rscons/builder" require_relative "rscons/builder"
require "rscons/cache" require_relative "rscons/cache"
require "rscons/environment" require_relative "rscons/environment"
require "rscons/varset" require_relative "rscons/varset"
require "rscons/version" require_relative "rscons/version"
require "rscons/monkey/string"
# default builders # default builders
require "rscons/builders/library" require_relative "rscons/builders/library"
require "rscons/builders/object" require_relative "rscons/builders/object"
require "rscons/builders/program" require_relative "rscons/builders/program"
# Namespace module for rscons classes # Namespace module for rscons classes
module Rscons module Rscons
@@ -37,4 +35,17 @@ module Rscons
end end
Cache.clear Cache.clear
end end
# Return whether the given path is an absolute filesystem path or not
# @param path [String] the path to examine
def self.absolute_path?(path)
path =~ %r{^(/|\w:[\\/])}
end
# Return a new path by changing the suffix in path to suffix.
# @param path [String] the path to alter
# @param suffix [String] the new filename suffix
def self.set_suffix(path, suffix)
path.sub(/\.[^.]*$/, suffix)
end
end end
+4 -4
View File
@@ -46,8 +46,8 @@ module Rscons
end end
def produces?(target, source, env) def produces?(target, source, env)
target.has_suffix?(env['OBJSUFFIX']) and KNOWN_SUFFIXES.find do |compiler, suffix_var| target.end_with?(*env['OBJSUFFIX']) and KNOWN_SUFFIXES.find do |compiler, suffix_var|
source.has_suffix?(env[suffix_var]) source.end_with?(*env[suffix_var])
end end
end end
@@ -55,10 +55,10 @@ module Rscons
vars = vars.merge({ vars = vars.merge({
'_TARGET' => target, '_TARGET' => target,
'_SOURCES' => sources, '_SOURCES' => sources,
'_DEPFILE' => target.set_suffix('.mf'), '_DEPFILE' => Rscons.set_suffix(target, '.mf'),
}) })
com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var| com_prefix = KNOWN_SUFFIXES.find do |compiler, suffix_var|
sources.first.has_suffix?(env[suffix_var]) sources.first.end_with?(*env[suffix_var])
end.tap do |v| end.tap do |v|
v.nil? and raise "Error: unknown input file type: #{sources.first.inspect}" v.nil? and raise "Error: unknown input file type: #{sources.first.inspect}"
end.first end.first
+2 -2
View File
@@ -21,9 +21,9 @@ module Rscons
return false unless objects return false unless objects
ld = if env["LD"] ld = if env["LD"]
env["LD"] env["LD"]
elsif sources.find {|s| s.has_suffix?(env["DSUFFIX"])} elsif sources.find {|s| s.end_with?(*env["DSUFFIX"])}
env["DC"] env["DC"]
elsif sources.find {|s| s.has_suffix?(env["CXXSUFFIX"])} elsif sources.find {|s| s.end_with?(*env["CXXSUFFIX"])}
env["CXX"] env["CXX"]
else else
env["CC"] env["CC"]
+55 -50
View File
@@ -85,16 +85,17 @@ module Rscons
end end
end end
# Check if a target is up to date # Check if target(s) are up to date
# @param target [String] The name of the target file. # @param targets [String, Array] The name(s) of the target file(s).
# @param command [Array] The command used to build the target. # @param command [String, Array] The command used to build the target.
# @param deps [Array] List of the target's dependency files. # @param deps [Array] List of the target's dependency files.
# @param env [Environment] The Rscons::Environment. # @param env [Environment] The Rscons::Environment.
# @param options [Hash] Optional options. Can contain the following keys: # @param options [Hash] Optional options. Can contain the following keys:
# :strict_deps:: # :strict_deps::
# Only consider a target up to date if its list of dependencies is # Only consider a target up to date if its list of dependencies is
# exactly equal (including order) to the cached list of dependencies # exactly equal (including order) to the cached list of dependencies
# @return true value if the target is up to date, meaning that: # @return true if the targets are all up to date, meaning that,
# for each target:
# - the target exists on disk # - the target exists on disk
# - the cache has information for the target # - the cache has information for the target
# - the target's checksum matches its checksum when it was last built # - the target's checksum matches its checksum when it was last built
@@ -104,65 +105,69 @@ module Rscons
# exactly equal to those cached # exactly equal to those cached
# - each cached dependency file's current checksum matches the checksum # - each cached dependency file's current checksum matches the checksum
# stored in the cache file # stored in the cache file
def up_to_date?(target, command, deps, env, options = {}) def up_to_date?(targets, command, deps, env, options = {})
# target file must exist on disk Array(targets).each do |target|
return false unless File.exists?(target) # target file must exist on disk
return false unless File.exists?(target)
# target must be registered in the cache # target must be registered in the cache
return false unless @cache[:targets].has_key?(target) return false unless @cache[:targets].has_key?(target)
# target must have the same checksum as when it was built last # target must have the same checksum as when it was built last
return false unless @cache[:targets][target][:checksum] == lookup_checksum(target) return false unless @cache[:targets][target][:checksum] == lookup_checksum(target)
# command used to build target must be identical # command used to build target must be identical
return false unless @cache[:targets][target][:command] == command return false unless @cache[:targets][target][:command] == command
cached_deps = @cache[:targets][target][:deps] || [] cached_deps = @cache[:targets][target][:deps] || []
cached_deps_fnames = cached_deps.map { |dc| dc[:fname] } cached_deps_fnames = cached_deps.map { |dc| dc[:fname] }
if options[:strict_deps] if options[:strict_deps]
# depedencies passed in must exactly equal those in the cache # depedencies passed in must exactly equal those in the cache
return false unless deps == cached_deps_fnames return false unless deps == cached_deps_fnames
else else
# all dependencies passed in must exist in cache (but cache may have more) # all dependencies passed in must exist in cache (but cache may have more)
return false unless (Set.new(deps) - Set.new(cached_deps_fnames)).empty? return false unless (Set.new(deps) - Set.new(cached_deps_fnames)).empty?
end end
# set of user dependencies must match # set of user dependencies must match
user_deps = env.get_user_deps(target) || [] user_deps = env.get_user_deps(target) || []
cached_user_deps = @cache[:targets][target][:user_deps] || [] cached_user_deps = @cache[:targets][target][:user_deps] || []
cached_user_deps_fnames = cached_user_deps.map { |dc| dc[:fname] } cached_user_deps_fnames = cached_user_deps.map { |dc| dc[:fname] }
return false unless user_deps == cached_user_deps_fnames return false unless user_deps == cached_user_deps_fnames
# all cached dependencies must have their checksums match # all cached dependencies must have their checksums match
(cached_deps + cached_user_deps).each do |dep_cache| (cached_deps + cached_user_deps).each do |dep_cache|
return false unless dep_cache[:checksum] == lookup_checksum(dep_cache[:fname]) return false unless dep_cache[:checksum] == lookup_checksum(dep_cache[:fname])
end
end end
true true
end end
# Store cache information about a target built by a builder # Store cache information about target(s) built by a builder
# @param target [String] The name of the target. # @param targets [String, Array] The name of the target(s) built.
# @param command [Array] The command used to build the target. # @param command [String, Array] The command used to build the target.
# @param deps [Array] List of dependencies for the target. # @param deps [Array] List of dependencies for the target.
# @param env [Environment] The Rscons::Environment. # @param env [Environment] The Rscons::Environment.
def register_build(target, command, deps, env) def register_build(targets, command, deps, env)
@cache[:targets][target.encode(__ENCODING__)] = { Array(targets).each do |target|
command: command, @cache[:targets][target.encode(__ENCODING__)] = {
checksum: calculate_checksum(target), command: command,
deps: deps.map do |dep| checksum: calculate_checksum(target),
{ deps: deps.map do |dep|
fname: dep.encode(__ENCODING__), {
checksum: lookup_checksum(dep), fname: dep.encode(__ENCODING__),
} checksum: lookup_checksum(dep),
end, }
user_deps: (env.get_user_deps(target) || []).map do |dep| end,
{ user_deps: (env.get_user_deps(target) || []).map do |dep|
fname: dep.encode(__ENCODING__), {
checksum: lookup_checksum(dep), fname: dep.encode(__ENCODING__),
} checksum: lookup_checksum(dep),
end, }
} end,
}
end
end end
# Return a list of targets that have been built # Return a list of targets that have been built
+3 -3
View File
@@ -102,7 +102,7 @@ module Rscons
# Return the file name to be built from source_fname with suffix suffix. # Return the file name to be built from source_fname with suffix suffix.
# This method takes into account the Environment's build directories. # This method takes into account the Environment's build directories.
def get_build_fname(source_fname, suffix) def get_build_fname(source_fname, suffix)
build_fname = source_fname.set_suffix(suffix).gsub('\\', '/') build_fname = Rscons.set_suffix(source_fname, suffix).gsub('\\', '/')
found_match = @build_dirs.find do |src_dir, obj_dir| found_match = @build_dirs.find do |src_dir, obj_dir|
if src_dir.is_a?(Regexp) if src_dir.is_a?(Regexp)
build_fname.sub!(src_dir, obj_dir) build_fname.sub!(src_dir, obj_dir)
@@ -111,7 +111,7 @@ module Rscons
end end
end end
if @build_root and not found_match if @build_root and not found_match
unless source_fname.absolute_path? or build_fname.start_with?("#{@build_root}/") unless Rscons.absolute_path?(source_fname) or build_fname.start_with?("#{@build_root}/")
build_fname = "#{@build_root}/#{build_fname}" build_fname = "#{@build_root}/#{build_fname}"
end end
end end
@@ -254,7 +254,7 @@ module Rscons
# Return a list of the converted file names. # Return a list of the converted file names.
def build_sources(sources, suffixes, cache, vars) def build_sources(sources, suffixes, cache, vars)
sources.map do |source| sources.map do |source|
if source.has_suffix?(suffixes) if source.end_with?(*suffixes)
source source
else else
converted = nil converted = nil
-25
View File
@@ -1,25 +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
# Return whether the string represents an absolute filesystem path
def absolute_path?
self =~ %r{^(/|\w:[\\/])}
end
end
+1 -1
View File
@@ -1,4 +1,4 @@
module Rscons module Rscons
# gem version # gem version
VERSION = "0.3.1" VERSION = "1.1.0"
end end
+35 -2
View File
@@ -218,9 +218,9 @@ describe Rscons do
end end
it 'allows Ruby classes as custom builders to be used to construct files' do it 'allows Ruby classes as custom builders to be used to construct files' do
test_dir('custom_builder') test_dir('custom_builder')
class MySource < Rscons::Builder class MySource < Rscons::Builder
def run(target, sources, user_deps, cache, env, vars = {}) def run(target, sources, cache, env, vars)
File.open(target, 'w') do |fh| File.open(target, 'w') do |fh|
fh.puts <<EOF fh.puts <<EOF
#define THE_VALUE 5678 #define THE_VALUE 5678
@@ -241,6 +241,39 @@ EOF
`./program`.should == "The value is 5678\n" `./program`.should == "The value is 5678\n"
end end
it 'supports custom builders with multiple targets' do
test_dir('custom_builder')
class CHGen < Rscons::Builder
def run(target, sources, cache, env, vars)
c_fname = target
h_fname = target.sub(/\.c$/, ".h")
unless cache.up_to_date?([c_fname, h_fname], "", sources, env)
puts "CHGen #{c_fname}"
File.open(c_fname, "w") {|fh| fh.puts "int THE_VALUE = 42;"}
File.open(h_fname, "w") {|fh| fh.puts "extern int THE_VALUE;"}
cache.register_build([c_fname, h_fname], "", sources, env)
end
target
end
end
env = Rscons::Environment.new do |env|
env.add_builder(CHGen.new)
env.CHGen("inc.c", ["program.c"])
env.Program("program", Dir["*.c"] + ["inc.c"])
end
lines.should == ["CHGen inc.c", "CC program.o", "CC inc.o", "LD program"]
File.exists?("inc.c").should be_true
File.exists?("inc.h").should be_true
`./program`.should == "The value is 42\n"
File.open("inc.c", "w") {|fh| fh.puts "int THE_VALUE = 33;"}
env.process
lines.should == ["CHGen inc.c"]
`./program`.should == "The value is 42\n"
end
it 'allows cloning Environment objects' do it 'allows cloning Environment objects' do
test_dir('clone_env') test_dir('clone_env')