Compare commits

..
32 Commits
Author SHA1 Message Date
josh aa795eea85 v1.9.0 2015-03-15 20:56:37 -04:00
josh 7dffef6bd0 update README.md 2015-02-15 19:52:33 -05:00
josh dce2114733 document Copy builder class 2015-02-15 18:42:37 -05:00
josh 0bbdda2d41 fix Command builder default short description 2015-02-15 18:41:41 -05:00
josh 3b8910075a use class instance variables instead of class variables 2015-02-15 18:38:09 -05:00
josh 34beda844e reorganize Rscons module class methods 2015-02-15 18:33:00 -05:00
josh 7a04bec2ff support phony targets - close #25 2015-02-15 18:29:33 -05:00
josh 7698fbf618 add "rscons" binary - close #24 2015-02-08 18:04:37 -05:00
josh 4eedea2563 add Copy builder as a copy of Install 2015-02-07 09:57:44 -05:00
josh f678803a5d add Install builder - close #6 2015-02-07 09:49:29 -05:00
josh 9c49a09f69 update :spec rake task to allow example pattern as an argument 2015-01-29 09:58:09 -05:00
josh 859f87ddee add a Directory builder - close #27 2015-01-29 09:31:00 -05:00
josh 73bc71a656 test expanding paths after a build target is registered from within a build hook 2015-01-28 17:15:55 -05:00
josh 29a8684f90 support registering multiple build targets with the same path - close #26 2015-01-28 17:10:36 -05:00
josh 02aff35222 allow overriding Command builder short description with CMD_DESC variable -- close #22 2015-01-24 09:40:07 -05:00
josh 897d31ebe0 fix Command builder example invocation 2015-01-21 14:07:28 -05:00
josh 76610a07a9 fix variable references that expand to arrays in build target sources - fix #20 2015-01-20 21:26:45 -05:00
josh cb3cc9a0de update 1.9.0 release notes 2015-01-20 21:18:19 -05:00
josh 0ea842fffe rework Preprocess builder to consider deep dependencies - fix #21 2015-01-20 21:16:21 -05:00
josh d28722a4bb fix Rscons.set_suffix to append the given suffix if the filename has none 2015-01-20 21:13:27 -05:00
josh 8142d0bbc8 add v1.8.1 release notes 2015-01-20 21:12:57 -05:00
josh a8023ae03c v1.8.1 2014-10-22 14:30:32 -04:00
josh 99ba015630 fix Environment#dump when construction variables are Symbols 2014-10-22 14:30:14 -04:00
josh dcf90cc12b v1.8.0 2014-10-22 10:36:28 -04:00
josh f93ca62c07 add v1.8.0 release notes 2014-10-22 10:31:52 -04:00
josh d9e3129ad5 add Environment#dump 2014-10-22 10:30:57 -04:00
josh d5e0475e6c minor documentation improvements 2014-10-21 17:00:38 -04:00
josh b05eed36eb add VarSet#to_h 2014-10-21 16:06:15 -04:00
josh 7ea0721cc2 add a Builder#run method which raises an error unless overridden 2014-10-16 16:52:26 -04:00
josh e654df008d add rdoc to bundle 2014-10-16 16:42:03 -04:00
Michael D. Lowis 92d4dd8396 Implemented SimpleBuilder and updated Environment#add_builder method to support it 2014-10-16 16:37:37 -04:00
Michael D. Lowis e279959d79 Added a command builder to the list of default builders 2014-10-13 12:27:05 -04:00
29 changed files with 1097 additions and 183 deletions

No files matched your search

+4 -1
View File
@@ -1,7 +1,7 @@
PATH PATH
remote: . remote: .
specs: specs:
rscons (1.7.0) rscons (1.9.0)
json (~> 1.0) json (~> 1.0)
GEM GEM
@@ -12,6 +12,8 @@ GEM
json (1.8.1) json (1.8.1)
multi_json (1.10.1) multi_json (1.10.1)
rake (10.3.2) rake (10.3.2)
rdoc (4.1.2)
json (~> 1.4)
redcarpet (3.1.2) redcarpet (3.1.2)
rspec (3.0.0) rspec (3.0.0)
rspec-core (~> 3.0.0) rspec-core (~> 3.0.0)
@@ -38,6 +40,7 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
rake rake
rdoc
redcarpet redcarpet
rscons! rscons!
rspec rspec
+101 -10
View File
@@ -1,6 +1,7 @@
# Rscons # Rscons
Software construction library inspired by SCons and implemented in Ruby Rscons is a software construction framework inspired by SCons and implemented
in Ruby.
[![Gem Version](https://badge.fury.io/rb/rscons.png)](http://badge.fury.io/rb/rscons) [![Gem Version](https://badge.fury.io/rb/rscons.png)](http://badge.fury.io/rb/rscons)
@@ -20,9 +21,10 @@ Or install it yourself as:
## Usage ## Usage
Rscons is a Ruby library. Rscons is implemented as a Ruby library and distributed as a gem.
It can be called from a standalone Ruby script or it can be used with rake and It also provides a "rscons" executable which can evaluate a build script (by
called from your Rakefile. default named Rsconsfile or Rsconsfile.rb).
It can also be used from rake or from a standalone Ruby script.
### Example: Building a C Program ### Example: Building a C Program
@@ -154,6 +156,31 @@ Rscons::Environment.new do |env|
end end
``` ```
### Example: Custom Builder Using Environment#add_builder()
The `add_builder` method of the `Rscons::Environment` class optionally allows
you to define and register a builder by providing a name and action block. This
can be useful if the builder you are trying to define is easily expressed as a
short ruby procedure. When `add_builder` is called in this manner a new builder
will be registered with the environment with the given name. When this builder
is used it will call the provided block in order to build the target.
```ruby
Rscons::Environment.new do |env|
env.add_builder(:JsonToYaml) do |target, sources, cache, env, vars|
unless cache.up_to_date?(target, :JsonToYaml, sources, env)
cache.mkdir_p(File.dirname(target))
File.open(target, 'w') do |f|
f.write(YAML.dump(JSON.load(IO.read(sources.first))))
end
cache.register_build(target, :JsonToYaml, sources, env)
end
target
end
env.JsonToYaml('foo.yml','foo.json')
end
```
### Example: Using different compilation flags for some sources ### Example: Using different compilation flags for some sources
```ruby ```ruby
@@ -210,16 +237,32 @@ end
Rscons ships with a number of builders: Rscons ships with a number of builders:
* CFile, which builds a C or C++ source file from a lex or yacc input file * Command, which executes a user-defined command to produce the target.
* Disassemble, which disassembles an object file to a disassembly listing * Copy, which is identical to Install.
* Library, which collects object files into a static library archive file * CFile, which builds a C or C++ source file from a lex or yacc input file.
* Object, which compiles source files to produce an object file * Disassemble, which disassembles an object file to a disassembly listing.
* Preprocess, which invokes the C/C++ preprocessor on a source file * Install, which installs files or directories to a specified destination.
* Program, which links object files to produce an executable * 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, If you want to create an Environment that does not contain any builders,
you can use the `exclude_builders` key to the Environment constructor. you can use the `exclude_builders` key to the Environment constructor.
#### Command
```ruby
env.Command(target, sources, "CMD" => command)
# Example
env.Command("docs.html", "docs.md",
"CMD" => ["pandoc", "-fmarkdown", "-thtml", "-o${_TARGET}", "${_SOURCES}"],
"CMD_DESC" => "PANDOC")
```
The command builder executes a user-defined command in order to produce the
desired target file based on the provided source files.
#### CFile #### CFile
```ruby ```ruby
@@ -242,6 +285,15 @@ env.Disassemble("module.dis", "module.o")
The Disassemble builder generates a disassembly listing using objdump from The Disassemble builder generates a disassembly listing using objdump from
and object file. and object file.
#### Install
```ruby
env.Install(destination, sources)
# Example
env.Install("dist/bin", "app.exe")
env.Install("dist/share", "share")
```
#### Library #### Library
```ruby ```ruby
@@ -333,6 +385,15 @@ Post-build hooks are only invoked if the build operation was a success.
Post-build hooks can invoke commands using the newly-built files, or register Post-build hooks can invoke commands using the newly-built files, or register
new build targets. new build targets.
### Phony Targets
A build target name given as a Symbol instead of a String is interpreted as a
"phony" target.
Phony targets operate similarly to normal build targets, except that a file is
not expected to be produced by the builder.
Phony targets will still be "rebuilt" if any source or the command is out of
date.
### Construction Variable Naming ### Construction Variable Naming
* uppercase strings - the default construction variables that Rscons uses * uppercase strings - the default construction variables that Rscons uses
@@ -345,6 +406,36 @@ http://rubydoc.info/github/holtrop/rscons/frames.
## Release Notes ## Release Notes
### v1.9.0
#### New Features
- #6 - add Install and Copy builders
- #22 - allow overriding Command builder short description with CMD_DESC variable
- #24 - add "rscons" executable
- #25 - add support for phony targets given as Symbols instead of Strings
- #26 - support registering multiple build targets with the same target name
- #27 - add Directory builder
#### Fixes
- #20 - fix variable references that expand to arrays in build target sources
- #21 - rework Preprocess builder to consider deep dependencies
- fix Rscons.set_suffix to append the given suffix if the filename has none
- remove ${CFLAGS} from default CPP_CMD
### v1.8.1
- fix Environment#dump when construction variables are symbols
### v1.8.0
- new Command builder to execute arbitrary user commands
- new SimpleBuilder class
- create new builders quickly by passing a block to Environment#add_builder
- improved YARD documentation
- add Environment#dump to debug Environment construction variables
### v1.7.0 ### v1.7.0
- allow build hooks to register new build targets - allow build hooks to register new build targets
+5 -1
View File
@@ -13,7 +13,11 @@ require "rake/clean"
CLEAN.include %w[build_test_run .yardoc doc coverage] CLEAN.include %w[build_test_run .yardoc doc coverage]
CLOBBER.include %w[pkg] CLOBBER.include %w[pkg]
RSpec::Core::RakeTask.new(:spec) RSpec::Core::RakeTask.new(:spec, :example_string) do |task, args|
if args.example_string
task.rspec_opts = %W[-e "#{args.example_string}" -f documentation]
end
end
YARD::Rake::YardocTask.new do |yard| YARD::Rake::YardocTask.new do |yard|
yard.files = ['lib/**/*.rb'] yard.files = ['lib/**/*.rb']
Executable
+3
View File
@@ -0,0 +1,3 @@
require "rscons/cli"
Rscons::Cli.run(ARGV)
+1
View File
@@ -0,0 +1 @@
{ "key": "value" }
+1
View File
@@ -0,0 +1 @@
#define BAR xyz42abc
+2
View File
@@ -0,0 +1,2 @@
#include "bar.h"
BAR
+108 -82
View File
@@ -7,19 +7,28 @@ require_relative "rscons/version"
# default builders # default builders
require_relative "rscons/builders/cfile" require_relative "rscons/builders/cfile"
require_relative "rscons/builders/command"
require_relative "rscons/builders/directory"
require_relative "rscons/builders/disassemble" require_relative "rscons/builders/disassemble"
require_relative "rscons/builders/install"
require_relative "rscons/builders/library" require_relative "rscons/builders/library"
require_relative "rscons/builders/object" require_relative "rscons/builders/object"
require_relative "rscons/builders/preprocess" require_relative "rscons/builders/preprocess"
require_relative "rscons/builders/program" require_relative "rscons/builders/program"
require_relative "rscons/builders/simple_builder"
# Namespace module for rscons classes # Namespace module for rscons classes
module Rscons module Rscons
# Names of the default builders which will be added to all newly created # Names of the default builders which will be added to all newly created
# {Environment} objects. # {Environment} objects.
DEFAULT_BUILDERS = [ DEFAULT_BUILDERS = [
:CFile, :CFile,
:Command,
:Copy,
:Directory,
:Disassemble, :Disassemble,
:Install,
:Library, :Library,
:Object, :Object,
:Preprocess, :Preprocess,
@@ -29,97 +38,114 @@ module Rscons
# Class to represent a fatal error while building a target. # Class to represent a fatal error while building a target.
class BuildError < RuntimeError; end class BuildError < RuntimeError; end
# Remove all generated files. class << self
def self.clean
cache = Cache.instance # Remove all generated files.
# remove all built files #
cache.targets.each do |target| # @return [void]
FileUtils.rm_f(target) def clean
end cache = Cache.instance
# remove all created directories if they are empty # remove all built files
cache.directories.sort {|a, b| b.size <=> a.size}.each do |directory| cache.targets.each do |target|
next unless File.directory?(directory) FileUtils.rm_f(target)
if (Dir.entries(directory) - ['.', '..']).empty?
Dir.rmdir(directory) rescue nil
end end
end # remove all created directories if they are empty
cache.clear cache.directories.sort {|a, b| b.size <=> a.size}.each do |directory|
end next unless File.directory?(directory)
if (Dir.entries(directory) - ['.', '..']).empty?
# Return whether the given path is an absolute filesystem path. Dir.rmdir(directory) rescue nil
#
# @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 end
if ENV["SHELL"] and ENV["SHELL"] != "" and test_shell[ENV["SHELL"], "-c"] end
[ENV["SHELL"], "-c"] cache.clear
elsif Object.const_get("RUBY_PLATFORM") =~ /mingw/ end
if test_shell["sh", "-c"]
# Using Rscons from MSYS should use MSYS's shell. # Return whether the given path is an absolute filesystem path.
["sh", "-c"] #
# @param path [String] the path to examine.
#
# @return [Boolean] Whether the given path is an absolute filesystem path.
def absolute_path?(path)
path =~ %r{^(/|\w:[\\/])}
end
# Return whether the given target is a phony target.
#
# @param target [Symbol, String] Target name.
#
# @return [Boolean] Whether the given target is a phony target.
def phony_target?(target)
target.is_a?(Symbol)
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 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 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 else
["cmd", "/c"] ["sh", "-c"]
end end
else
["sh", "-c"]
end end
end end
end
# Return an Array containing a command used to execute commands. # Return an Array containing a command used to execute commands.
# #
# This will normally be an empty Array, but on Windows if Rscons detects # This will normally be an empty Array, but on Windows if Rscons detects
# that it is running in MSYS then ["env"] will be returned. # that it is running in MSYS then ["env"] will be returned.
# #
# @return [Array<String>] Command used to execute commands. # @return [Array<String>] Command used to execute commands.
def self.command_executer def command_executer
@@command_executer ||= @command_executer ||=
if Object.const_get("RUBY_PLATFORM") =~ /mingw/ if Object.const_get("RUBY_PLATFORM") =~ /mingw/
if ENV.keys.find {|key| key =~ /MSYS/} if ENV.keys.find {|key| key =~ /MSYS/}
begin begin
if IO.popen(["env", "echo", "success"]) {|io| io.read.strip} == "success" if IO.popen(["env", "echo", "success"]) {|io| io.read.strip} == "success"
["env"] ["env"]
end
rescue
end end
rescue
end end
end end || []
end || [] end
end
# Set the command executer array.
#
# @param val [Array<String>] Command used to execute commands.
#
# @return [Array<String>] Command used to execute commands.
def command_executer=(val)
@command_executer = val
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
end end
+21 -3
View File
@@ -9,6 +9,8 @@ module Rscons
# Return the name of the builder. # Return the name of the builder.
# #
# If not overridden this defaults to the last component of the class name. # If not overridden this defaults to the last component of the class name.
#
# @return [String] The name of the builder.
def name def name
self.class.name.split(":").last self.class.name.split(":").last
end end
@@ -44,7 +46,7 @@ module Rscons
# file name from a given source file name. # file name from a given source file name.
# #
# @param target [String] The target file name. # @param target [String] The target file name.
# @param source [String, Array] The source file name(s). # @param source [String] The source file name.
# @param env [Environment] The Environment. # @param env [Environment] The Environment.
# #
# @return [Boolean] # @return [Boolean]
@@ -54,6 +56,20 @@ module Rscons
false false
end 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)
raise "This method must be overridden in a subclass"
end
# Check if the cache is up to date for the target and if not execute the # Check if the cache is up to date for the target and if not execute the
# build command. # build command.
# #
@@ -71,8 +87,10 @@ module Rscons
# The name of the target on success or false on failure. # The name of the target on success or false on failure.
def standard_build(short_cmd_string, target, command, sources, env, cache) def standard_build(short_cmd_string, target, command, sources, env, cache)
unless cache.up_to_date?(target, command, sources, env) unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target)) unless Rscons.phony_target?(target)
FileUtils.rm_f(target) cache.mkdir_p(File.dirname(target))
FileUtils.rm_f(target)
end
return false unless env.execute(short_cmd_string, command) return false unless env.execute(short_cmd_string, command)
cache.register_build(target, command, sources, env) cache.register_build(target, command, sources, env)
end end
+33
View File
@@ -0,0 +1,33 @@
module Rscons
module Builders
# Execute a command that will produce the given target based on the given
# sources.
#
# @since 1.8.0
#
# Example:
# env.Command("docs.html", "docs.md",
# CMD => %w[pandoc -fmarkdown -thtml -o${_TARGET} ${_SOURCES}])
class Command < Builder
# 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,
})
command = env.build_command("${CMD}", vars)
cmd_desc = vars["CMD_DESC"] || "Command"
standard_build("#{cmd_desc} #{target}", target, command, sources, env, cache)
end
end
end
end
+31
View File
@@ -0,0 +1,31 @@
module Rscons
module Builders
# The Directory builder creates a directory.
class Directory < Builder
# 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)
if File.directory?(target)
target
elsif File.exists?(target)
$stderr.puts "Error: `#{target}' already exists and is not a directory"
false
else
puts "Directory #{target}"
cache.mkdir_p(target)
target
end
end
end
end
end
+64
View File
@@ -0,0 +1,64 @@
require "pathname"
module Rscons
module Builders
# The Install builder copies files/directories to new locations.
class Install < Builder
# 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)
target_is_dir = (sources.length > 1) ||
Dir.exists?(sources.first) ||
Dir.exists?(target)
outdir = target_is_dir ? target : File.dirname(target)
# Collect the list of files to copy over.
file_map = {}
if target_is_dir
sources.each do |src|
if Dir.exists? src
Dir.glob("#{src}/**/*", File::FNM_DOTMATCH).select do |f|
File.file?(f)
end.each do |subfile|
subpath = Pathname.new(subfile).relative_path_from(Pathname.new(src)).to_s
file_map[subfile] = "#{outdir}/#{subpath}"
end
else
file_map[src] = "#{outdir}/#{File.basename(src)}"
end
end
else
file_map[sources.first] = target
end
printed_message = false
file_map.each do |src, dest|
# Check the cache and copy if necessary
unless cache.up_to_date?(dest, :Copy, [src], env)
unless printed_message
puts "#{name} #{target}"
printed_message = true
end
cache.mkdir_p(File.dirname(dest))
FileUtils.cp(src, dest, :preserve => true)
end
cache.register_build(dest, :Copy, [src], env)
end
target if (target_is_dir ? Dir.exists?(target) : File.exists?(target))
end
end
# The Copy builder is identical to the Install builder.
class Copy < Install; end
end
end
+1 -1
View File
@@ -62,7 +62,7 @@ module Rscons
# file name from a given source file name. # file name from a given source file name.
# #
# @param target [String] The target file name. # @param target [String] The target file name.
# @param source [String, Array] The source file name(s). # @param source [String] The source file name.
# @param env [Environment] The Environment. # @param env [Environment] The Environment.
# #
# @return [Boolean] # @return [Boolean]
+28 -8
View File
@@ -1,7 +1,10 @@
require "fileutils"
module Rscons module Rscons
module Builders module Builders
# The Preprocess builder invokes the C preprocessor # The Preprocess builder invokes the C preprocessor
class Preprocess < Builder class Preprocess < Builder
# Return default construction variables for the builder. # Return default construction variables for the builder.
# #
# @param env [Environment] The Environment using the builder. # @param env [Environment] The Environment using the builder.
@@ -9,7 +12,9 @@ module Rscons
# @return [Hash] Default construction variables for the builder. # @return [Hash] Default construction variables for the builder.
def default_variables(env) def default_variables(env)
{ {
"CPP_CMD" => ["${_PREPROCESS_CC}", "-E", "-o", "${_TARGET}", "-I${CPPPATH}", "${CPPFLAGS}", "${CFLAGS}", "${_SOURCES}"], "CPP_CMD" => %w[
${_PREPROCESS_CC} -E ${_PREPROCESS_DEPGEN}
-o ${_TARGET} -I${CPPPATH} ${CPPFLAGS} ${_SOURCES}],
} }
end end
@@ -24,17 +29,32 @@ module Rscons
# @return [String,false] # @return [String,false]
# Name of the target file on success or false on failure. # Name of the target file on success or false on failure.
def run(target, sources, cache, env, vars) def run(target, sources, cache, env, vars)
pp_cc = if sources.find {|s| s.end_with?(*env.expand_varref("${CXXSUFFIX}", vars))} if sources.find {|s| s.end_with?(*env.expand_varref("${CXXSUFFIX}", vars))}
"${CXX}" pp_cc = "${CXX}"
else depgen = "${CXXDEPGEN}"
"${CC}" else
end pp_cc = "${CC}"
depgen = "${CCDEPGEN}"
end
vars = vars.merge("_PREPROCESS_CC" => pp_cc, vars = vars.merge("_PREPROCESS_CC" => pp_cc,
"_PREPROCESS_DEPGEN" => depgen,
"_TARGET" => target, "_TARGET" => target,
"_SOURCES" => sources) "_SOURCES" => sources,
"_DEPFILE" => Rscons.set_suffix(target, env.expand_varref("${DEPFILESUFFIX}", vars)))
command = env.build_command("${CPP_CMD}", vars) command = env.build_command("${CPP_CMD}", vars)
standard_build("Preprocess #{target}", target, command, sources, env, cache) unless cache.up_to_date?(target, command, sources, env)
cache.mkdir_p(File.dirname(target))
return false unless env.execute("Preprocess #{target}", command)
deps = sources
if File.exists?(vars["_DEPFILE"])
deps += Environment.parse_makefile_deps(vars["_DEPFILE"], nil)
FileUtils.rm_f(vars["_DEPFILE"])
end
cache.register_build(target, command, deps.uniq, env)
end
target
end end
end end
end end
end end
+39
View File
@@ -0,0 +1,39 @@
module Rscons
module Builders
# A Generic builder class whose name and operation is defined at
# instantiation.
#
# @since 1.8.0
class SimpleBuilder < Builder
# @return [String] The name of this builder.
attr_reader :name
# Create a new builder with the given name and action.
#
# @param name [String,Symbol] The name of the builder when registered.
# @param block [Block]
# The action to perform when the builder is processed. The provided
# block must return the target file on success or false on failure.
# The provided block should have the same signature as {Builder#run}.
def initialize(name, &block)
@name = name.to_s
@block = block
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)
@block.call(target, sources, cache, env, vars)
end
end
end
end
+59 -16
View File
@@ -56,6 +56,9 @@ module Rscons
# Name of the file to store cache information in # Name of the file to store cache information in
CACHE_FILE = ".rsconscache" CACHE_FILE = ".rsconscache"
# Prefix for phony cache entries.
PHONY_PREFIX = ":PHONY:"
# Create a Cache object and load in the previous contents from the cache # Create a Cache object and load in the previous contents from the cache
# file. # file.
def initialize def initialize
@@ -63,17 +66,23 @@ module Rscons
end end
# Remove the cache file. # Remove the cache file.
#
# @return [void]
def clear def clear
FileUtils.rm_f(CACHE_FILE) FileUtils.rm_f(CACHE_FILE)
initialize! initialize!
end end
# Clear the cached file checksums. # Clear the cached file checksums.
#
# @return [void]
def clear_checksum_cache! def clear_checksum_cache!
@lookup_checksums = {} @lookup_checksums = {}
end end
# Write the cache to disk to be loaded next time. # Write the cache to disk to be loaded next time.
#
# @return [void]
def write def write
if @dirty || (@cache["version"] != VERSION) if @dirty || (@cache["version"] != VERSION)
@cache["version"] = VERSION @cache["version"] = VERSION
@@ -86,9 +95,15 @@ module Rscons
# Check if target(s) are up to date. # Check if target(s) are up to date.
# #
# @param targets [String, Array] The name(s) of the target file(s). # @param targets [Symbol, String, Array<String>]
# @param command [String, Array] The command used to build the target. # The name(s) of the target file(s).
# @param deps [Array] List of the target's dependency files. # @param command [String, Array, Hash]
# The command used to build the target. The command parameter can
# actually be a String, Array, or Hash and could contain information
# other than just the actual command used to build the target. For the
# purposes of the Cache, any difference in the command argument will
# trigger a rebuild.
# @param deps [Array<String>] List of the target's dependency files.
# @param env [Environment] The Rscons::Environment. # @param env [Environment] The Rscons::Environment.
# @param options [Hash] Optional options. # @param options [Hash] Optional options.
# @option options [Boolean] :strict_deps # @option options [Boolean] :strict_deps
@@ -109,19 +124,25 @@ module Rscons
# stored in the cache file # stored in the cache file
def up_to_date?(targets, command, deps, env, options = {}) def up_to_date?(targets, command, deps, env, options = {})
Array(targets).each do |target| Array(targets).each do |target|
# target file must exist on disk cache_key = get_cache_key(target)
return false unless File.exists?(target)
unless Rscons.phony_target?(target)
# target file must exist on disk
return false unless File.exists?(target)
end
# 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?(cache_key)
# target must have the same checksum as when it was built last unless Rscons.phony_target?(target)
return false unless @cache["targets"][target]["checksum"] == lookup_checksum(target) # target must have the same checksum as when it was built last
return false unless @cache["targets"][cache_key]["checksum"] == lookup_checksum(target)
end
# command used to build target must be identical # command used to build target must be identical
return false unless @cache["targets"][target]["command"] == Digest::MD5.hexdigest(command.inspect) return false unless @cache["targets"][cache_key]["command"] == Digest::MD5.hexdigest(command.inspect)
cached_deps = @cache["targets"][target]["deps"] || [] cached_deps = @cache["targets"][cache_key]["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
@@ -133,7 +154,7 @@ module Rscons
# set of user dependencies must match # set of user dependencies must match
user_deps = env.get_user_deps(target) || [] user_deps = env.get_user_deps(target) || []
cached_user_deps = @cache["targets"][target]["user_deps"] || [] cached_user_deps = @cache["targets"][cache_key]["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
@@ -148,17 +169,24 @@ module Rscons
# Store cache information about target(s) built by a builder. # Store cache information about target(s) built by a builder.
# #
# @param targets [String, Array] The name of the target(s) built. # @param targets [Symbol, String, Array<String>]
# @param command [String, Array] The command used to build the target. # The name of the target(s) built.
# @param deps [Array] List of dependencies for the target. # @param command [String, Array, Hash]
# The command used to build the target. The command parameter can
# actually be a String, Array, or Hash and could contain information
# other than just the actual command used to build the target. For the
# purposes of the Cache, any difference in the command argument will
# trigger a rebuild.
# @param deps [Array<String>] List of dependencies for the target.
# @param env [Environment] The {Rscons::Environment}. # @param env [Environment] The {Rscons::Environment}.
# #
# @return [void] # @return [void]
def register_build(targets, command, deps, env) def register_build(targets, command, deps, env)
Array(targets).each do |target| Array(targets).each do |target|
@cache["targets"][target] = { target_checksum = Rscons.phony_target?(target) ? "" : calculate_checksum(target)
@cache["targets"][get_cache_key(target)] = {
"command" => Digest::MD5.hexdigest(command.inspect), "command" => Digest::MD5.hexdigest(command.inspect),
"checksum" => calculate_checksum(target), "checksum" => target_checksum,
"deps" => deps.map do |dep| "deps" => deps.map do |dep|
{ {
"fname" => dep, "fname" => dep,
@@ -212,6 +240,21 @@ module Rscons
private private
# Return a String key based on the target name to use in the on-disk cache.
#
# @param target_name [Symbol, String]
# Target name.
#
# @return [String]
# Key name.
def get_cache_key(target_name)
if Rscons.phony_target?(target_name)
PHONY_PREFIX + target_name.to_s
else
target_name
end
end
# Create a Cache object and load in the previous contents from the cache # Create a Cache object and load in the previous contents from the cache
# file. # file.
def initialize! def initialize!
+78
View File
@@ -0,0 +1,78 @@
require "rscons"
require "optparse"
module Rscons
# Command-Line Interface functionality.
module Cli
# Default files to look for to execute if none specified.
DEFAULT_RSCONSFILES = %w[Rsconsfile Rsconsfile.rb]
class << self
# Run the Rscons CLI.
#
# @param argv [Array]
# Command-line parameters.
#
# @return [void]
def run(argv)
argv = argv.dup
rsconsfile = nil
OptionParser.new do |opts|
opts.banner = "Usage: #{$0} [options]"
opts.separator ""
opts.separator "Options:"
opts.on("-c", "--clean", "Perform clean operation") do
Rscons.clean
exit 0
end
opts.on("-f FILE", "Execute FILE (default Rsconsfile)") do |f|
rsconsfile = f
end
opts.on_tail("--version", "Show version") do
puts "Rscons version #{Rscons::VERSION}"
exit 0
end
opts.on_tail("-h", "--help", "Show this help.") do
puts opts
exit 0
end
end.parse!(argv)
if rsconsfile
unless File.exists?(rsconsfile)
$stderr.puts "Cannot read #{rsconsfile}"
exit 1
end
else
rsconsfile = DEFAULT_RSCONSFILES.find do |f|
File.exists?(f)
end
unless rsconsfile
$stderr.puts "Could not find the Rsconsfile to execute."
$stderr.puts "Looked in: [#{DEFAULT_RSCONSFILES.join(", ")}]"
exit 1
end
end
begin
load rsconsfile
rescue Rscons::BuildError => e
$stderr.puts e.message
exit 1
end
exit 0
end
end
end
end
+91 -44
View File
@@ -10,7 +10,7 @@ module Rscons
# @return [Hash] Set of !{"builder_name" => builder_object} pairs. # @return [Hash] Set of !{"builder_name" => builder_object} pairs.
attr_reader :builders attr_reader :builders
# :command, :short, or :off # @return [Symbol] :command, :short, or :off
attr_accessor :echo attr_accessor :echo
# @return [String, nil] The build root. # @return [String, nil] The build root.
@@ -79,7 +79,7 @@ module Rscons
# #
# Any options that #initialize receives can also be specified here. # Any options that #initialize receives can also be specified here.
# #
# @return a new {Environment} object. # @return [Environment] The newly created {Environment} object.
def clone(options = {}) def clone(options = {})
clone = options[:clone] || Set[:variables, :builders] clone = options[:clone] || Set[:variables, :builders]
clone = Set[:variables, :builders, :build_root, :build_dirs, :build_hooks] if clone == :all clone = Set[:variables, :builders, :build_root, :build_dirs, :build_hooks] if clone == :all
@@ -120,10 +120,32 @@ module Rscons
# Add a {Builder} object to the Environment. # Add a {Builder} object to the Environment.
# #
# @param builder [Builder] The {Builder} object to add. # @overload add_builder(builder)
#
# Add the given builder to the Environment.
#
# @param builder [Builder] An instance of the builder to register.
#
# @overload add_builder(builder,&action)
#
# Create a new {Builders::SimpleBuilder} instance and add it to the
# environment.
#
# @since 1.8.0
#
# @param builder [String,Symbol]
# The name of the builder to add.
#
# @param action [Block]
# A block that will be called when the builder is executed to generate
# a target file. The provided block should have the same prototype as
# {Rscons::Builder#run}.
# #
# @return [void] # @return [void]
def add_builder(builder) def add_builder(builder, &action)
if not builder.is_a? Rscons::Builder
builder = Rscons::Builders::SimpleBuilder.new(builder, &action)
end
@builders[builder.name] = builder @builders[builder.name] = builder
var_defs = builder.default_variables(self) var_defs = builder.default_variables(self)
if var_defs if var_defs
@@ -184,8 +206,13 @@ module Rscons
# #
# Source files from src_dir will produce object files under obj_dir. # Source files from src_dir will produce object files under obj_dir.
# #
# @param src_dir [String, Regexp] Path to the source directory. # @param src_dir [String, Regexp]
# @param obj_dir [String] Path to the object directory. # Path to the source directory. If a Regexp is given, it will be matched
# to source file names.
# @param obj_dir [String]
# Path to the object directory. If a Regexp is given as src_dir, then
# obj_dir can contain backreferences to groups matched from the source
# file name.
# #
# @return [void] # @return [void]
def build_dir(src_dir, obj_dir) def build_dir(src_dir, obj_dir)
@@ -223,25 +250,27 @@ module Rscons
build_fname build_fname
end end
# Access a construction variable or environment option. # Get a construction variable's value.
# #
# @see VarSet#[] # @see VarSet#[]
def [](*args) def [](*args)
@varset.send(:[], *args) @varset.send(:[], *args)
end end
# Set a construction variable or environment option. # Set a construction variable's value.
# #
# @see VarSet#[]= # @see VarSet#[]=
def []=(*args) def []=(*args)
@varset.send(:[]=, *args) @varset.send(:[]=, *args)
end end
# Add a set of construction variables or environment options. # Add a set of construction variables to the Environment.
# #
# @see VarSet#append # @param values [VarSet, Hash] New set of variables.
def append(*args) #
@varset.append(*args) # @return [void]
def append(values)
@varset.append(values)
end end
# Build all build targets specified in the Environment. # Build all build targets specified in the Environment.
@@ -251,33 +280,35 @@ module Rscons
# #
# @return [void] # @return [void]
def process def process
expand_paths!
while @targets.size > 0 while @targets.size > 0
expand_paths!
targets = @targets targets = @targets
@targets = {} @targets = {}
cache = Cache.instance cache = Cache.instance
cache.clear_checksum_cache! cache.clear_checksum_cache!
targets_processed = {} targets_processed = Set.new
process_target = proc do |target| process_target = proc do |target|
targets_processed[target] ||= begin unless targets_processed.include?(target)
targets[target][:sources].each do |src| targets_processed << target
if targets.include?(src) and not targets_processed.include?(src) targets[target].each do |target_params|
process_target.call(src) target_params[:sources].each do |src|
if targets.include?(src) and not targets_processed.include?(src)
process_target.call(src)
end
end
result = run_builder(target_params[:builder],
target,
target_params[:sources],
cache,
target_params[:vars] || {})
unless result
raise BuildError.new("Failed to build #{target}")
end end
end end
result = run_builder(targets[target][:builder],
target,
targets[target][:sources],
cache,
targets[target][:vars] || {})
unless result
raise BuildError.new("Failed to build #{target}")
end
result
end end
end end
begin begin
targets.each do |target, target_params| targets.each_key do |target|
process_target.call(target) process_target.call(target)
end end
ensure ensure
@@ -287,6 +318,8 @@ module Rscons
end end
# Clear all targets registered for the Environment. # Clear all targets registered for the Environment.
#
# @return [void]
def clear_targets def clear_targets
@targets = {} @targets = {}
end end
@@ -373,7 +406,8 @@ module Rscons
# #
# @return [void] # @return [void]
def add_target(target, builder, sources, vars, args) def add_target(target, builder, sources, vars, args)
@targets[target] = { @targets[target] ||= []
@targets[target] << {
builder: builder, builder: builder,
sources: sources, sources: sources,
vars: vars, vars: vars,
@@ -410,8 +444,9 @@ module Rscons
# #
# This method is used internally by Rscons builders. # This method is used internally by Rscons builders.
# #
# @param sources [Array] List of source files to build. # @param sources [Array<String>] List of source files to build.
# @param suffixes [Array] List of suffixes to try to convert source files into. # @param suffixes [Array<String>]
# List of suffixes to try to convert source files into.
# @param cache [Cache] The Cache. # @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder. # @param vars [Hash] Extra variables to pass to the builder.
# #
@@ -440,7 +475,7 @@ module Rscons
# #
# @param builder [Builder] The Builder to use. # @param builder [Builder] The Builder to use.
# @param target [String] The target output file. # @param target [String] The target output file.
# @param sources [Array] List of source files. # @param sources [Array<String>] List of source files.
# @param cache [Cache] The Cache. # @param cache [Cache] The Cache.
# @param vars [Hash] Extra variables to pass to the builder. # @param vars [Hash] Extra variables to pass to the builder.
# #
@@ -506,10 +541,10 @@ module Rscons
# Parse command-line flags for compilation/linking options into separate # Parse command-line flags for compilation/linking options into separate
# construction variables. # construction variables.
# #
# The parsed construction variables are returned in a Hash instead of # For {#parse_flags}, the parsed construction variables are returned in a
# merging them directly to the Environment. They can be merged with # Hash instead of merging them directly to the Environment. They can be
# {#merge_flags}. The {#parse_flags!} version immediately merges the parsed # merged with {#merge_flags}. The {#parse_flags!} version immediately
# flags as well. # merges the parsed flags as well.
# #
# Example: # Example:
# # Import FreeType build options # # Import FreeType build options
@@ -628,6 +663,15 @@ module Rscons
end end
end end
# Print the Environment's construction variables for debugging.
def dump
varset_hash = @varset.to_h
varset_hash.keys.sort_by(&:to_s).each do |var|
var_str = var.is_a?(Symbol) ? var.inspect : var
puts "#{var_str} => #{varset_hash[var].inspect}"
end
end
private private
# Expand target and source paths before invoking builders. # Expand target and source paths before invoking builders.
@@ -638,14 +682,16 @@ module Rscons
# #
# @return [void] # @return [void]
def expand_paths! def expand_paths!
@targets = @targets.reduce({}) do |result, (target, target_params)| @targets = @targets.reduce({}) do |result, (target, target_params_list)|
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_path(target) if @build_root
target = expand_varref(target) target = expand_varref(target)
result[target] = target_params.merge(sources: sources) result[target] = target_params_list.map do |target_params|
sources = target_params[:sources].map do |source|
source = expand_path(source) if @build_root
expand_varref(source)
end.flatten
target_params.merge(sources: sources)
end
result result
end end
end end
@@ -655,7 +701,8 @@ module Rscons
# This method is used internally by Rscons builders. # This method is used internally by Rscons builders.
# #
# @param mf_fname [String] File name of the Makefile to read. # @param mf_fname [String] File name of the Makefile to read.
# @param target [String] Name of the target to gather dependencies for. # @param target [String, nil]
# Name of the target to gather dependencies for, nil for any/all.
# #
# @return [Array<String>] Paths of dependency files. # @return [Array<String>] Paths of dependency files.
def self.parse_makefile_deps(mf_fname, target) def self.parse_makefile_deps(mf_fname, target)
@@ -668,7 +715,7 @@ module Rscons
buildup += ' ' + line buildup += ' ' + line
if buildup =~ /^(.*): (.*)$/ if buildup =~ /^(.*): (.*)$/
mf_target, mf_deps = $1.strip, $2 mf_target, mf_deps = $1.strip, $2
if mf_target == target if target.nil? or mf_target == target
deps += mf_deps.split(' ').map(&:strip) deps += mf_deps.split(' ').map(&:strip)
end end
end end
+23 -1
View File
@@ -58,7 +58,7 @@ module Rscons
# #
# @param values [VarSet, Hash] New set of variables. # @param values [VarSet, Hash] New set of variables.
# #
# @return [self] # @return [VarSet] Returns self.
def append(values) def append(values)
coa! coa!
if values.is_a?(VarSet) if values.is_a?(VarSet)
@@ -116,11 +116,33 @@ module Rscons
expand_varref(varref[*lambda_args], lambda_args) expand_varref(varref[*lambda_args], lambda_args)
elsif varref.nil? elsif varref.nil?
nil nil
elsif varref.is_a?(Symbol)
varref
else else
raise "Unknown varref type: #{varref.class} (#{varref.inspect})" raise "Unknown varref type: #{varref.class} (#{varref.inspect})"
end end
end end
# Return a Hash containing all variables in the VarSet.
#
# @since 1.8.0
#
# This method is not terribly efficient. It is intended to be used only by
# debugging code to dump out a VarSet's variables.
#
# @return [Hash] All variables in the VarSet.
def to_h
result = deep_dup(@my_vars)
@coa_vars.reduce(result) do |result, coa_vars|
coa_vars.each_pair do |key, value|
unless result.include?(key)
result[key] = deep_dup(value)
end
end
result
end
end
private private
# Move all VarSet variables into the copy-on-access list. # Move all VarSet variables into the copy-on-access list.
+1 -1
View File
@@ -1,4 +1,4 @@
module Rscons module Rscons
# gem version # gem version
VERSION = "1.7.0" VERSION = "1.9.0"
end end
+1
View File
@@ -24,5 +24,6 @@ Gem::Specification.new do |gem|
gem.add_development_dependency "rake" gem.add_development_dependency "rake"
gem.add_development_dependency "simplecov" gem.add_development_dependency "simplecov"
gem.add_development_dependency "yard" gem.add_development_dependency "yard"
gem.add_development_dependency "rdoc"
gem.add_development_dependency "redcarpet" gem.add_development_dependency "redcarpet"
end end
+263
View File
@@ -249,6 +249,27 @@ describe Rscons do
] ]
end end
it 'supports simple builders' do
test_dir('json_to_yaml')
Rscons::Environment.new do |env|
require 'json'
require 'yaml'
env.add_builder(:JsonToYaml) do |target, sources, cache, env, vars|
unless cache.up_to_date?(target, :JsonToYaml, sources, env)
cache.mkdir_p(File.dirname(target))
File.open(target, 'w') do |f|
f.write(YAML.dump(JSON.load(IO.read(sources.first))))
end
cache.register_build(target, :JsonToYaml, sources, env)
end
target
end
env.JsonToYaml('foo.yml','foo.json')
end
expect(File.exists?('foo.yml')).to be_truthy
expect(IO.read('foo.yml')).to eq("---\nkey: value\n")
end
it 'cleans built files' do it 'cleans built files' do
test_dir('build_dir') test_dir('build_dir')
Rscons::Environment.new do |env| Rscons::Environment.new do |env|
@@ -700,4 +721,246 @@ EOF
expect(`./two_sources`).to eq "This is a C program with two sources.\n" expect(`./two_sources`).to eq "This is a C program with two sources.\n"
end end
it "supports dumping an Environment's construction variables" do
test_dir("simple")
env = Rscons::Environment.new do |env|
env["CFLAGS"] += %w[-O2 -fomit-frame-pointer]
env[:foo] = :bar
end
env.dump
result = lines
expect(result.include?(%{:foo => :bar})).to be_truthy
expect(result.include?(%{CFLAGS => ["-O2", "-fomit-frame-pointer"]})).to be_truthy
expect(result.include?(%{CPPPATH => []})).to be_truthy
end
it "considers deep dependencies when deciding whether to rerun Preprocess builder" do
test_dir("preprocess")
env = Rscons::Environment.new do |env|
env.Preprocess("pp", "foo.h")
end
expect(File.read("pp")).to match(%r{xyz42abc}m)
expect(lines).to eq(["Preprocess pp"])
env.Preprocess("pp", "foo.h")
env.process
expect(lines).to eq([])
File.open("bar.h", "w") do |fh|
fh.puts "#define BAR abc88xyz"
end
$ttt = true
env.Preprocess("pp", "foo.h")
env.process
expect(lines).to eq(["Preprocess pp"])
expect(File.read("pp")).to match(%r{abc88xyz}m)
end
it "allows construction variable references which expand to arrays in sources of a build target" do
test_dir("simple")
Rscons::Environment.new do |env|
env["sources"] = Dir["*.c"]
env.Program("simple", "${sources}")
end
expect(File.exists?("simple.o")).to be_truthy
expect(`./simple`).to eq "This is a simple C program\n"
end
it "supports registering multiple build targets with the same target path" do
test_dir("build_dir")
Rscons::Environment.new do |env|
env["CPPPATH"] << "src/two"
env.Object("one.o", "src/one/one.c")
env.Object("one.o", "src/two/two.c")
end
expect(File.exists?("one.o")).to be_truthy
expect(lines).to eq([
"CC one.o",
"CC one.o",
])
end
it "expands target and source paths when builders are registered in build hooks" do
test_dir("build_dir")
Rscons::Environment.new do |env|
env["CPPPATH"] << "src/two"
env.Object("one.o", "src/one/one.c")
env.add_post_build_hook do |build_op|
if build_op[:target] == "one.o"
env["MODULE"] = "two"
env.Object("${MODULE}.o", "src/${MODULE}/${MODULE}.c")
end
end
end
expect(File.exists?("one.o")).to be_truthy
expect(File.exists?("two.o")).to be_truthy
expect(lines).to eq([
"CC one.o",
"CC two.o",
])
end
context "Directory builder" do
it "creates the requested directory" do
test_dir("simple")
Rscons::Environment.new do |env|
env.Directory("teh_dir")
end
expect(File.directory?("teh_dir")).to be_truthy
expect(lines).to eq(["Directory teh_dir"])
end
it "succeeds when the requested directory already exists" do
test_dir("simple")
FileUtils.mkdir("teh_dir")
Rscons::Environment.new do |env|
env.Directory("teh_dir")
end
expect(File.directory?("teh_dir")).to be_truthy
expect(lines).to eq([])
end
it "fails when the target path is a file" do
test_dir("simple")
FileUtils.touch("teh_dir")
expect do
Rscons::Environment.new do |env|
env.Directory("teh_dir")
end
end.to raise_error /Failed to build teh_dir/
expect(lines).to eq([
"Error: `teh_dir' already exists and is not a directory",
])
end
end
context "Install buildler" do
let(:base_env) do
test_dir("build_dir")
Rscons::Environment.new do |env|
env["CPPPATH"] = Dir["src/**/"]
env["sources"] = Dir["src/**/*.c"]
env.Program("simple.exe", "${sources}")
end
end
it "copies a file to the target file name" do
env = base_env.clone do |env|
lines
env.Install("inst.exe", "simple.exe")
end
expect(lines).to eq(["Install inst.exe"])
env.Install("inst.exe", "simple.exe")
env.process
expect(lines).to eq([])
expect(File.exists?("inst.exe")).to be_truthy
expect(File.read("inst.exe", mode: "rb")).to eq(File.read("simple.exe", mode: "rb"))
FileUtils.rm("inst.exe")
env.Install("inst.exe", "simple.exe")
env.process
expect(lines).to eq(["Install inst.exe"])
end
it "operates the same as a Copy builder" do
env = base_env.clone do |env|
lines
env.Copy("inst.exe", "simple.exe")
end
expect(lines).to eq(["Copy inst.exe"])
env.Copy("inst.exe", "simple.exe")
env.process
expect(lines).to eq([])
expect(File.exists?("inst.exe")).to be_truthy
expect(File.read("inst.exe", mode: "rb")).to eq(File.read("simple.exe", mode: "rb"))
FileUtils.rm("inst.exe")
env.Copy("inst.exe", "simple.exe")
env.process
expect(lines).to eq(["Copy inst.exe"])
end
it "copies a file to the target directory name" do
env = base_env.clone do |env|
lines
env.Directory("inst")
env.Install("inst", "simple.exe")
end
expect(lines).to eq([
"Directory inst",
"Install inst",
])
env.Install("inst", "simple.exe")
env.process
expect(lines).to eq([])
expect(File.exists?("inst/simple.exe")).to be_truthy
expect(File.read("inst/simple.exe", mode: "rb")).to eq(File.read("simple.exe", mode: "rb"))
end
it "copies a directory to the non-existent target directory name" do
env = base_env.clone do |env|
lines
env.Install("dist/src", "src")
end
expect(lines).to eq(["Install dist/src"])
env.Install("dist/src", "src")
env.process
expect(lines).to eq([])
%w[src/one/one.c src/two/two.c src/two/two.h].each do |f|
expect(File.exists?("dist/#{f}")).to be_truthy
expect(File.read("dist/#{f}", mode: "rb")).to eq(File.read(f, mode: "rb"))
end
end
it "copies a directory to the existent target directory name" do
env = base_env.clone do |env|
lines
env.Directory("dist/src")
env.Install("dist/src", "src")
end
expect(lines).to eq([
"Directory dist/src",
"Install dist/src",
])
env.Install("dist/src", "src")
env.process
expect(lines).to eq([])
%w[src/one/one.c src/two/two.c src/two/two.h].each do |f|
expect(File.exists?("dist/#{f}")).to be_truthy
expect(File.read("dist/#{f}", mode: "rb")).to eq(File.read(f, mode: "rb"))
end
FileUtils.rm("dist/src/two/two.c")
env.Install("dist/src", "src")
env.process
expect(lines).to eq(["Install dist/src"])
end
end
context "phony targets" do
it "allows specifying a Symbol as a target name and reruns the builder if the sources or command have changed" do
test_dir("simple")
env = Rscons::Environment.new do |env|
env.add_builder(:Checker) do |target, sources, cache, env, vars|
unless cache.up_to_date?(target, :Checker, sources, env)
puts "Checker #{sources.first}" if env.echo != :off
cache.register_build(target, :Checker, sources, env)
end
target
end
env.Program("simple.exe", "simple.c")
env.Checker(:checker, "simple.exe")
end
expect(lines).to eq(["CC simple.o", "LD simple.exe", "Checker simple.exe"])
env.clone do |env|
env.Checker(:checker, "simple.exe")
end
expect(lines).to eq([])
File.open("simple.exe", "w") do |fh|
fh.puts "Changed simple.exe"
end
env.clone do |env|
env.Checker(:checker, "simple.exe")
end
expect(lines).to eq(["Checker simple.exe"])
end
end
end end
+9
View File
@@ -0,0 +1,9 @@
module Rscons
describe Builder do
describe "#run" do
it "raises an error if called directly and not through a subclass" do
expect{subject.run(:target, :sources, :cache, :env, :vars)}.to raise_error /This method must be overridden in a subclass/
end
end
end
end
+18
View File
@@ -0,0 +1,18 @@
module Rscons
module Builders
describe Command do
let(:command) { ['pandoc', '-fmarkdown', '-thtml', '-o${_TARGET}', '${_SOURCES}'] }
let(:env) {Environment.new}
subject {Command.new}
it "invokes the command to build the target" do
expected_cmd = ['pandoc', '-fmarkdown', '-thtml', '-ofoo.html', 'foo.md']
expect(subject).to receive(:standard_build).with("PANDOC foo.html", "foo.html", expected_cmd, ["foo.md"], env, :cache)
subject.run("foo.html", ["foo.md"], :cache, env,
"CMD" => command, "CMD_DESC" => "PANDOC")
end
end
end
end
+34 -4
View File
@@ -1,18 +1,48 @@
module Rscons module Rscons
module Builders module Builders
describe Preprocess do describe Preprocess do
let(:env) {Environment.new} let(:env) {Environment.new}
subject {Preprocess.new} subject {Preprocess.new}
it "supports overriding CC construction variable" do 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) cache = double(Cache)
subject.run("module.pp", ["module.c"], :cache, env, "CC" => "my_cpp") command = %w[my_cpp -E -MMD -MF module.mf -o module.pp module.c]
expect(cache).to receive(:up_to_date?).with("module.pp", command, %w[module.c], env).and_return(false)
expect(cache).to receive(:mkdir_p).with(".")
expect(env).to receive(:execute).with("Preprocess module.pp", command).and_return(true)
expect(File).to receive(:exists?).with("module.mf").and_return(true)
expect(Environment).to receive(:parse_makefile_deps).with("module.mf", nil).and_return(%w[module.c one.h two.h])
expect(FileUtils).to receive(:rm_f).with("module.mf")
expect(cache).to receive(:register_build).with("module.pp", command, %w[module.c one.h two.h], env)
expect(subject.run("module.pp", ["module.c"], cache, env, "CC" => "my_cpp")).to eq("module.pp")
end end
it "supports overriding CPP_CMD construction variable" do 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) cache = double(Cache)
subject.run("module.pp", ["module.c"], :cache, env, "CPP_CMD" => ["my_cpp", "${_SOURCES}"]) command = %w[my_cpp module.c]
expect(cache).to receive(:up_to_date?).with("module.pp", command, %w[module.c], env).and_return(false)
expect(cache).to receive(:mkdir_p).with(".")
expect(env).to receive(:execute).with("Preprocess module.pp", command).and_return(true)
expect(File).to receive(:exists?).with("module.mf").and_return(true)
expect(Environment).to receive(:parse_makefile_deps).with("module.mf", nil).and_return(%w[module.c one.h two.h])
expect(FileUtils).to receive(:rm_f).with("module.mf")
expect(cache).to receive(:register_build).with("module.pp", command, %w[module.c one.h two.h], env)
expect(subject.run("module.pp", ["module.c"], cache, env, "CPP_CMD" => ["my_cpp", "${_SOURCES}"])).to eq("module.pp")
end end
it "returns false if executing the preprocessor fails" do
cache = double(Cache)
command = %w[gcc -E -MMD -MF module.mf -o module.pp module.c]
expect(cache).to receive(:up_to_date?).with("module.pp", command, %w[module.c], env).and_return(false)
expect(cache).to receive(:mkdir_p).with(".")
expect(env).to receive(:execute).with("Preprocess module.pp", command).and_return(false)
expect(subject.run("module.pp", ["module.c"], cache, env, {})).to eq(false)
end
end end
end end
end end
@@ -0,0 +1,19 @@
module Rscons
module Builders
describe SimpleBuilder do
let(:env) {Environment.new}
it "should create a new builder with the given name (as a symbol) and action" do
builder = Rscons::Builders::SimpleBuilder.new(:Foo) { 0x1234 }
expect(builder.name).to eq("Foo")
expect(builder.run(1,2,3,4,5)).to eq(0x1234)
end
it "should create a new builder with the given name (as a string) and action" do
builder = Rscons::Builders::SimpleBuilder.new("Foo") { 0x1234 }
expect(builder.name).to eq("Foo")
expect(builder.run(1,2,3,4,5)).to eq(0x1234)
end
end
end
end
+11 -4
View File
@@ -72,6 +72,13 @@ module Rscons
env.add_builder(Rscons::Builders::Object.new) env.add_builder(Rscons::Builders::Object.new)
expect(env.builders.keys).to eq ["Object"] expect(env.builders.keys).to eq ["Object"]
end end
it "adds a new simple builder to the list of builders" do
env = Environment.new(exclude_builders: true)
expect(env.builders.keys).to eq []
env.add_builder(:Foo) {}
expect(env.builders.keys).to eq ["Foo"]
end
end end
describe "#get_build_fname" do describe "#get_build_fname" do
@@ -298,10 +305,10 @@ module Rscons
env.Object("target.o", ["src1.c", "src2.c"], var: "val") env.Object("target.o", ["src1.c", "src2.c"], var: "val")
target = env.instance_variable_get(:@targets)["target.o"] target = env.instance_variable_get(:@targets)["target.o"]
expect(target).to_not be_nil expect(target).to_not be_nil
expect(target[:builder].is_a?(Builder)).to be_truthy expect(target[0][:builder].is_a?(Builder)).to be_truthy
expect(target[:sources]).to eq ["src1.c", "src2.c"] expect(target[0][:sources]).to eq ["src1.c", "src2.c"]
expect(target[:vars]).to eq({var: "val"}) expect(target[0][:vars]).to eq({var: "val"})
expect(target[:args]).to eq [] expect(target[0][:args]).to eq []
end end
it "raises an error when vars is not a Hash" do it "raises an error when vars is not a Hash" do
+31
View File
@@ -1,5 +1,6 @@
module Rscons module Rscons
describe VarSet do describe VarSet do
describe '#initialize' do describe '#initialize' do
it "initializes variables from a Hash" do it "initializes variables from a Hash" do
v = VarSet.new({"one" => 1, "two" => :two}) v = VarSet.new({"one" => 1, "two" => :two})
@@ -168,5 +169,35 @@ module Rscons
expect { v.expand_varref("${bad}", :lambda_args) }.to raise_error /I do not know how to expand a variable reference to a Hash/ 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
describe "#to_h" do
it "returns a Hash of all variables in the VarSet" do
v = VarSet.new({"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [3, 6], "qux" => {a: :b}})
v2 = v.merge("baz" => "qux", "qax" => [4, 7])
v3 = v2.merge("fuz" => nil)
v4 = v3.clone
v4["fuz"] = :fuzzy
v4["foo"] = 43
expect(v.to_h).to eq({
"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [3, 6], "qux" => {a: :b},
})
expect(v2.to_h).to eq({
"fuz" => "a string", "foo" => 42, "bar" => :baz,
"qax" => [4, 7], "qux" => {a: :b}, "baz" => "qux",
})
expect(v3.to_h).to eq({
"fuz" => nil, "foo" => 42, "bar" => :baz,
"qax" => [4, 7], "qux" => {a: :b}, "baz" => "qux",
})
expect(v4.to_h).to eq({
"fuz" => :fuzzy, "foo" => 43, "bar" => :baz,
"qax" => [4, 7], "qux" => {a: :b}, "baz" => "qux",
})
expect(VarSet.new.to_h).to eq({})
end
end
end end
end end
+17 -7
View File
@@ -24,13 +24,23 @@ describe Rscons do
end end
end end
describe ".set_suffix" do
it "changes the suffix to the new one" do
expect(Rscons.set_suffix("foo.c", ".h")).to eq("foo.h")
end
it "appends a suffix if the given file name does not have one" do
expect(Rscons.set_suffix("bazz", ".d")).to eq("bazz.d")
end
end
describe ".get_system_shell" do describe ".get_system_shell" do
before(:each) do before(:each) do
Rscons.class_variable_set(:@@shell, nil) Rscons.instance_variable_set(:@shell, nil)
end end
after(:each) do after(:each) do
Rscons.class_variable_set(:@@shell, nil) Rscons.instance_variable_set(:@shell, nil)
end end
it "uses the SHELL environment variable if it tests successfully" do it "uses the SHELL environment variable if it tests successfully" do
@@ -70,11 +80,11 @@ describe Rscons do
context "command executer" do context "command executer" do
describe ".command_executer" do describe ".command_executer" do
before(:each) do before(:each) do
Rscons.class_variable_set(:@@command_executer, nil) Rscons.instance_variable_set(:@command_executer, nil)
end end
after(:each) do after(:each) do
Rscons.class_variable_set(:@@command_executer, nil) Rscons.instance_variable_set(:@command_executer, nil)
end end
it "returns ['env'] if mingw platform in MSYS and 'env' works" do it "returns ['env'] if mingw platform in MSYS and 'env' works" do
@@ -105,10 +115,10 @@ describe Rscons do
end end
describe ".command_executer=" do describe ".command_executer=" do
it "overrides the value of @@command_executer" do it "overrides the value of @command_executer" do
Rscons.class_variable_set(:@@command_executer, ["env"]) Rscons.instance_variable_set(:@command_executer, ["env"])
Rscons.command_executer = [] Rscons.command_executer = []
expect(Rscons.class_variable_get(:@@command_executer)).to eq([]) expect(Rscons.instance_variable_get(:@command_executer)).to eq([])
end end
end end
end end