rscons 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +42 -0
- data/Rakefile +21 -0
- data/build_tests/build_dir/build.rb +5 -0
- data/build_tests/build_dir/src/one/one.c +6 -0
- data/build_tests/build_dir/src/two/two.c +7 -0
- data/build_tests/build_dir/src/two/two.h +6 -0
- data/build_tests/clone_env/build.rb +11 -0
- data/build_tests/clone_env/src/program.c +6 -0
- data/build_tests/custom_builder/build.rb +16 -0
- data/build_tests/custom_builder/program.c +7 -0
- data/build_tests/header/build.rb +3 -0
- data/build_tests/header/header.c +7 -0
- data/build_tests/header/header.h +6 -0
- data/build_tests/simple/build.rb +4 -0
- data/build_tests/simple/simple.c +6 -0
- data/build_tests/simple_cc/build.rb +4 -0
- data/build_tests/simple_cc/simple.cc +8 -0
- data/lib/rscons.rb +20 -0
- data/lib/rscons/builder.rb +20 -0
- data/lib/rscons/builders/object.rb +69 -0
- data/lib/rscons/builders/program.rb +48 -0
- data/lib/rscons/cache.rb +147 -0
- data/lib/rscons/environment.rb +227 -0
- data/lib/rscons/monkey/module.rb +7 -0
- data/lib/rscons/monkey/string.rb +19 -0
- data/lib/rscons/varset.rb +83 -0
- data/lib/rscons/version.rb +4 -0
- data/rscons.gemspec +29 -0
- data/spec/build_tests_spec.rb +146 -0
- data/spec/spec_helper.rb +1 -0
- metadata +232 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Josh Holtrop
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# Rscons
|
2
|
+
|
3
|
+
Software construction library inspired by SCons and implemented in Ruby
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'rscons'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install rscons
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
RScons::Environment.new do |env|
|
22
|
+
env.Program('program', Dir['*.c'])
|
23
|
+
end
|
24
|
+
|
25
|
+
main_env = RScons::Environment.new do |env|
|
26
|
+
env.build_dir('src', 'build/main')
|
27
|
+
env.Program('program', Dir['src/**/*.cc'])
|
28
|
+
end
|
29
|
+
|
30
|
+
debug_env = main_env.clone do |env|
|
31
|
+
env.build_dir('src', 'build/debug')
|
32
|
+
env['CFLAGS'] = ['-g', '-O0']
|
33
|
+
env.Program('program-debug', Dir['src/**/*.cc'])
|
34
|
+
end
|
35
|
+
|
36
|
+
## Contributing
|
37
|
+
|
38
|
+
1. Fork it
|
39
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
40
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
41
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
42
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require "bundler"
|
2
|
+
begin
|
3
|
+
Bundler.setup(:default, :development)
|
4
|
+
rescue Bundler::BundlerError => e
|
5
|
+
raise LoadError.new("Unable to setup Bundler; you might need to `bundle install`: #{e.message}")
|
6
|
+
end
|
7
|
+
|
8
|
+
require "bundler/gem_tasks"
|
9
|
+
require "rspec/core/rake_task"
|
10
|
+
require "rake/clean"
|
11
|
+
require "yard"
|
12
|
+
|
13
|
+
CLEAN.include 'build_tests_run'
|
14
|
+
|
15
|
+
RSpec::Core::RakeTask.new(:spec)
|
16
|
+
|
17
|
+
YARD::Rake::YardocTask.new do |yard|
|
18
|
+
yard.files = ['lib/**/*.rb']
|
19
|
+
end
|
20
|
+
|
21
|
+
task :default => :spec
|
@@ -0,0 +1,11 @@
|
|
1
|
+
debug = Rscons::Environment.new do |env|
|
2
|
+
env.build_dir('src', 'debug')
|
3
|
+
env['CFLAGS'] = '-O2'
|
4
|
+
env['CPPFLAGS'] = '-DSTRING="Debug Version"'
|
5
|
+
env.Program('program-debug', Dir['src/*.c'])
|
6
|
+
end
|
7
|
+
|
8
|
+
release = debug.clone('CPPFLAGS' => '-DSTRING="Release Version"') do |env|
|
9
|
+
env.build_dir('src', 'release')
|
10
|
+
env.Program('program-release', Dir['src/*.c'])
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class MySource < Rscons::Builder
|
2
|
+
def run(target, sources, cache, env)
|
3
|
+
File.open(target, 'w') do |fh|
|
4
|
+
fh.puts <<EOF
|
5
|
+
#define THE_VALUE 5678
|
6
|
+
EOF
|
7
|
+
end
|
8
|
+
target
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
Rscons::Environment.new(echo: :short) do |env|
|
13
|
+
env.add_builder(MySource.new)
|
14
|
+
env.MySource('inc.h', [])
|
15
|
+
env.Program('program', Dir['*.c'])
|
16
|
+
end
|
data/lib/rscons.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "rscons/builder"
|
2
|
+
require "rscons/cache"
|
3
|
+
require "rscons/environment"
|
4
|
+
require "rscons/varset"
|
5
|
+
require "rscons/version"
|
6
|
+
|
7
|
+
require "rscons/monkey/module"
|
8
|
+
require "rscons/monkey/string"
|
9
|
+
|
10
|
+
# default builders
|
11
|
+
require "rscons/builders/object"
|
12
|
+
require "rscons/builders/program"
|
13
|
+
|
14
|
+
# Namespace module for rscons classes
|
15
|
+
module Rscons
|
16
|
+
DEFAULT_BUILDERS = [
|
17
|
+
Object,
|
18
|
+
Program,
|
19
|
+
]
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rscons
|
2
|
+
# Class to hold an object that knows how to build a certain type of file.
|
3
|
+
class Builder
|
4
|
+
# Return a set of default variable values for the Environment to use
|
5
|
+
# unless the user overrides any.
|
6
|
+
# @param env [Environment] The Environment.
|
7
|
+
def default_variables(env)
|
8
|
+
{}
|
9
|
+
end
|
10
|
+
|
11
|
+
# Return whether this builder object is capable of producing a given target
|
12
|
+
# file name from a given source file name.
|
13
|
+
# @param target [String] The target file name.
|
14
|
+
# @param source [String, Array] The source file name(s).
|
15
|
+
# @param env [Environment] The Environment.
|
16
|
+
def produces?(target, source, env)
|
17
|
+
false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Rscons
|
2
|
+
# A default RScons builder which knows how to produce an object file from
|
3
|
+
# various types of source files.
|
4
|
+
class Object < Builder
|
5
|
+
def default_variables(env)
|
6
|
+
{
|
7
|
+
'OBJSUFFIX' => '.o',
|
8
|
+
|
9
|
+
'AS' => '$CC',
|
10
|
+
'ASFLAGS' => [],
|
11
|
+
'ASSUFFIX' => '.S',
|
12
|
+
'ASPPPATH' => '$CPPPATH',
|
13
|
+
'ASPPFLAGS' => '$CPPFLAGS',
|
14
|
+
'ASDEPGEN' => ['-MMD', '-MF', '$DEPFILE'],
|
15
|
+
'ASCOM' => ['$AS', '-c', '-o', '$TARGET', '$ASDEPGEN', '-I$[ASPPPATH]', '$ASPPFLAGS', '$ASFLAGS', '$SOURCES'],
|
16
|
+
|
17
|
+
'CPPFLAGS' => [],
|
18
|
+
'CPPPATH' => [],
|
19
|
+
|
20
|
+
'CC' => 'gcc',
|
21
|
+
'CFLAGS' => [],
|
22
|
+
'CSUFFIX' => '.c',
|
23
|
+
'CCDEPGEN' => ['-MMD', '-MF', '$DEPFILE'],
|
24
|
+
'CCCOM' => ['$CC', '-c', '-o', '$TARGET', '$CCDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CFLAGS', '$SOURCES'],
|
25
|
+
|
26
|
+
'CXX' => 'g++',
|
27
|
+
'CXXFLAGS' => [],
|
28
|
+
'CXXSUFFIX' => '.cc',
|
29
|
+
'CXXDEPGEN' => ['-MMD', '-MF', '$DEPFILE'],
|
30
|
+
'CXXCOM' =>['$CXX', '-c', '-o', '$TARGET', '$CXXDEPGEN', '-I$[CPPPATH]', '$CPPFLAGS', '$CXXFLAGS', '$SOURCES'],
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def produces?(target, source, env)
|
35
|
+
target.has_suffix?(env['OBJSUFFIX']) and (
|
36
|
+
source.has_suffix?(env['ASSUFFIX']) or
|
37
|
+
source.has_suffix?(env['CSUFFIX']) or
|
38
|
+
source.has_suffix?(env['CXXSUFFIX']))
|
39
|
+
end
|
40
|
+
|
41
|
+
def run(target, sources, cache, env)
|
42
|
+
vars = {
|
43
|
+
'TARGET' => target,
|
44
|
+
'SOURCES' => sources,
|
45
|
+
'DEPFILE' => target.set_suffix('.mf'),
|
46
|
+
}
|
47
|
+
com_prefix = if sources.first.has_suffix?(env['ASSUFFIX'])
|
48
|
+
'AS'
|
49
|
+
elsif sources.first.has_suffix?(env['CSUFFIX'])
|
50
|
+
'CC'
|
51
|
+
elsif sources.first.has_suffix?(env['CXXSUFFIX'])
|
52
|
+
'CXX'
|
53
|
+
else
|
54
|
+
raise "Error: unknown input file type: #{sources.first.inspect}"
|
55
|
+
end
|
56
|
+
command = env.build_command(env["#{com_prefix}COM"], vars)
|
57
|
+
unless cache.up_to_date?(target, command, sources)
|
58
|
+
return false unless env.execute("#{com_prefix} #{target}", command)
|
59
|
+
deps = sources
|
60
|
+
if File.exists?(vars['DEPFILE'])
|
61
|
+
deps += env.parse_makefile_deps(vars['DEPFILE'], target)
|
62
|
+
FileUtils.rm_f(vars['DEPFILE'])
|
63
|
+
end
|
64
|
+
cache.register_build(target, command, deps.uniq)
|
65
|
+
end
|
66
|
+
target
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Rscons
|
2
|
+
# A default RScons builder that knows how to link object files into an
|
3
|
+
# executable program.
|
4
|
+
class Program < Builder
|
5
|
+
def default_variables(env)
|
6
|
+
{
|
7
|
+
'LD' => nil,
|
8
|
+
'OBJSUFFIX' => '.o',
|
9
|
+
'LIBSUFFIX' => '.a',
|
10
|
+
'LDFLAGS' => [],
|
11
|
+
'LIBPATHS' => [],
|
12
|
+
'LIBS' => [],
|
13
|
+
'LDCOM' => ['$LD', '-o', '$TARGET', '$LDFLAGS', '$SOURCES', '-L$[LIBPATHS]', '-l$[LIBS]']
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def run(target, sources, cache, env)
|
18
|
+
# convert sources to object file names
|
19
|
+
objects = sources.map do |source|
|
20
|
+
if source.has_suffix?([env['OBJSUFFIX'], env['LIBSUFFIX']])
|
21
|
+
source
|
22
|
+
else
|
23
|
+
o_file = env.get_build_fname(source, env['OBJSUFFIX', :string])
|
24
|
+
builder = env.builders.values.find { |b| b.produces?(o_file, source, env) }
|
25
|
+
builder or raise "No builder found to convert input source #{source.inspect} to an object file."
|
26
|
+
builder.run(o_file, [source], cache, env) or break
|
27
|
+
end
|
28
|
+
end
|
29
|
+
if objects
|
30
|
+
use_cxx = sources.map do |s|
|
31
|
+
s.has_suffix?(env['CXXSUFFIX'])
|
32
|
+
end.any?
|
33
|
+
ld_alt = use_cxx ? env['CXX'] : env['CC']
|
34
|
+
vars = {
|
35
|
+
'TARGET' => target,
|
36
|
+
'SOURCES' => objects,
|
37
|
+
'LD' => env['LD'] || ld_alt,
|
38
|
+
}
|
39
|
+
command = env.build_command(env['LDCOM'], vars)
|
40
|
+
unless cache.up_to_date?(target, command, objects)
|
41
|
+
return false unless env.execute("LD #{target}", command)
|
42
|
+
cache.register_build(target, command, objects)
|
43
|
+
end
|
44
|
+
target
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/rscons/cache.rb
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'fileutils'
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'set'
|
5
|
+
require 'rscons/version'
|
6
|
+
|
7
|
+
module Rscons
|
8
|
+
# The Cache class keeps track of file checksums, build target commands and
|
9
|
+
# dependencies in a YAML file which persists from one invocation to the next.
|
10
|
+
# Example cache:
|
11
|
+
# {
|
12
|
+
# version: '1.2.3',
|
13
|
+
# targets: {
|
14
|
+
# 'program' => {
|
15
|
+
# 'checksum' => 'A1B2C3D4',
|
16
|
+
# 'command' => ['gcc', '-o', 'program', 'program.o'],
|
17
|
+
# 'deps' => [
|
18
|
+
# {
|
19
|
+
# 'fname' => 'program.o',
|
20
|
+
# 'checksum' => '87654321',
|
21
|
+
# }
|
22
|
+
# ],
|
23
|
+
# }
|
24
|
+
# 'program.o' => {
|
25
|
+
# 'checksum' => '87654321',
|
26
|
+
# 'command' => ['gcc', '-c', '-o', 'program.o', 'program.c'],
|
27
|
+
# 'deps' => [
|
28
|
+
# {
|
29
|
+
# 'fname' => 'program.c',
|
30
|
+
# 'checksum' => '456789ABC',
|
31
|
+
# },
|
32
|
+
# {
|
33
|
+
# 'fname' => 'program.h',
|
34
|
+
# 'checksum' => '7979764643',
|
35
|
+
# }
|
36
|
+
# ]
|
37
|
+
# }
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
class Cache
|
41
|
+
#### Constants
|
42
|
+
|
43
|
+
# Name of the file to store cache information in
|
44
|
+
CACHE_FILE = '.rsconscache'
|
45
|
+
|
46
|
+
#### Class Methods
|
47
|
+
|
48
|
+
# Remove the cache file
|
49
|
+
def self.clear
|
50
|
+
FileUtils.rm_f(CACHE_FILE)
|
51
|
+
end
|
52
|
+
|
53
|
+
#### Instance Methods
|
54
|
+
|
55
|
+
# Create a Cache object and load in the previous contents from the cache
|
56
|
+
# file.
|
57
|
+
def initialize
|
58
|
+
@cache = YAML.load(File.read(CACHE_FILE)) rescue {
|
59
|
+
targets: {},
|
60
|
+
version: VERSION,
|
61
|
+
}
|
62
|
+
@lookup_checksums = {}
|
63
|
+
end
|
64
|
+
|
65
|
+
# Write the cache to disk to be loaded next time.
|
66
|
+
def write
|
67
|
+
File.open(CACHE_FILE, 'w') do |fh|
|
68
|
+
fh.puts(YAML.dump(@cache))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check if a target is up to date
|
73
|
+
# @param target [String] The name of the target file.
|
74
|
+
# @param command [Array] The command used to build the target.
|
75
|
+
# @param deps [Array] List of the target's dependency files.
|
76
|
+
# @param options [Hash] Optional options. Can contain the following keys:
|
77
|
+
# :strict_deps::
|
78
|
+
# Only consider a target up to date if its list of dependencies is
|
79
|
+
# exactly equal (including order) to the cached list of dependencies
|
80
|
+
# @return true value if the target is up to date, meaning that:
|
81
|
+
# - the target exists on disk
|
82
|
+
# - the cache has information for the target
|
83
|
+
# - the command used to build the target is the same as last time
|
84
|
+
# - all dependencies listed are also listed in the cache, or, if
|
85
|
+
# :strict_deps was given in options, the list of dependencies is
|
86
|
+
# exactly equal to those cached
|
87
|
+
# - each cached dependency file's current checksum matches the checksum
|
88
|
+
# stored in the cache file
|
89
|
+
def up_to_date?(target, command, deps, options = {})
|
90
|
+
# target file must exist on disk
|
91
|
+
return false unless File.exists?(target)
|
92
|
+
|
93
|
+
# target must be registered in the cache
|
94
|
+
return false unless @cache[:targets].has_key?(target)
|
95
|
+
|
96
|
+
# command used to build target must be identical
|
97
|
+
return false unless @cache[:targets][target][:command] == command
|
98
|
+
|
99
|
+
cached_deps = @cache[:targets][target][:deps].map { |dc| dc[:fname] }
|
100
|
+
if options[:strict_deps]
|
101
|
+
# depedencies passed in must exactly equal those in the cache
|
102
|
+
return false unless deps == cached_deps
|
103
|
+
else
|
104
|
+
# all dependencies passed in must exist in cache (but cache may have more)
|
105
|
+
return false unless (Set.new(deps) - Set.new(cached_deps)).empty?
|
106
|
+
end
|
107
|
+
|
108
|
+
# all cached dependencies must have their checksums match
|
109
|
+
@cache[:targets][target][:deps].map do |dep_cache|
|
110
|
+
dep_cache[:checksum] == lookup_checksum(dep_cache[:fname])
|
111
|
+
end.all?
|
112
|
+
end
|
113
|
+
|
114
|
+
# Store cache information about a target built by a builder
|
115
|
+
# @param target [String] The name of the target.
|
116
|
+
# @param command [Array] The command used to build the target.
|
117
|
+
# @param deps [Array] List of dependencies for the target.
|
118
|
+
def register_build(target, command, deps)
|
119
|
+
@cache[:targets][target] = {
|
120
|
+
command: command,
|
121
|
+
checksum: calculate_checksum(target),
|
122
|
+
deps: deps.map do |dep|
|
123
|
+
{
|
124
|
+
fname: dep,
|
125
|
+
checksum: lookup_checksum(dep),
|
126
|
+
}
|
127
|
+
end
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
# Private Instance Methods
|
132
|
+
private
|
133
|
+
|
134
|
+
# Return a file's checksum, or the previously calculated checksum for
|
135
|
+
# the same file
|
136
|
+
# @param file [String] The file name.
|
137
|
+
def lookup_checksum(file)
|
138
|
+
@lookup_checksums[file] || calculate_checksum(file)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Calculate and return a file's checksum
|
142
|
+
# @param file [String] The file name.
|
143
|
+
def calculate_checksum(file)
|
144
|
+
@lookup_checksums[file] = Digest::MD5.hexdigest(File.read(file)).encode(__ENCODING__) rescue ''
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|