rscons 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ require 'set'
2
+ require 'fileutils'
3
+
4
+ module Rscons
5
+ # The Environment class is the main programmatic interface to RScons. It
6
+ # contains a collection of construction variables, options, builders, and
7
+ # rules for building targets.
8
+ class Environment
9
+ # [Array] of {Builder} objects.
10
+ attr_reader :builders
11
+
12
+ # Create an Environment object.
13
+ # @param variables [Hash]
14
+ # The variables hash can contain both construction variables, which are
15
+ # uppercase strings (such as "CC" or "LDFLAGS"), and RScons options,
16
+ # which are lowercase symbols (such as :echo).
17
+ # If a block is given, the Environment object is yielded to the block and
18
+ # when the block returns, the {#process} method is automatically called.
19
+ def initialize(variables = {})
20
+ @varset = VarSet.new(variables)
21
+ @targets = {}
22
+ @builders = {}
23
+ @build_dirs = {}
24
+ @varset[:exclude_builders] ||= []
25
+ unless @varset[:exclude_builders] == :all
26
+ exclude_builders = Set.new(@varset[:exclude_builders] || [])
27
+ DEFAULT_BUILDERS.each do |builder_class|
28
+ unless exclude_builders.include?(builder_class.short_name)
29
+ add_builder(builder_class.new)
30
+ end
31
+ end
32
+ end
33
+ (@varset[:builders] || []).each do |builder|
34
+ add_builder(builder)
35
+ end
36
+ @varset[:echo] ||= :command
37
+
38
+ if block_given?
39
+ yield self
40
+ self.process
41
+ end
42
+ end
43
+
44
+ # Make a copy of the Environment object.
45
+ # The cloned environment will contain a copy of all environment options,
46
+ # construction variables, builders, and build directories. It will not
47
+ # contain a copy of the targets.
48
+ # If a block is given, the Environment object is yielded to the block and
49
+ # when the block returns, the {#process} method is automatically called.
50
+ def clone(variables = {})
51
+ env = Environment.new()
52
+ @builders.each do |builder_name, builder|
53
+ env.add_builder(builder)
54
+ end
55
+ @build_dirs.each do |src_dir, obj_dir|
56
+ env.build_dir(src_dir, obj_dir)
57
+ end
58
+ env.append(@varset)
59
+ env.append(variables)
60
+
61
+ if block_given?
62
+ yield env
63
+ env.process
64
+ end
65
+ env
66
+ end
67
+
68
+ # Add a {Builder} object to the Environment.
69
+ def add_builder(builder)
70
+ @builders[builder.class.short_name] = builder
71
+ var_defs = builder.default_variables(self)
72
+ if var_defs
73
+ var_defs.each_pair do |var, val|
74
+ @varset[var] ||= val
75
+ end
76
+ end
77
+ end
78
+
79
+ # Specify a build directory for this Environment.
80
+ # Source files from src_dir will produce object files under obj_dir.
81
+ def build_dir(src_dir, obj_dir)
82
+ @build_dirs[src_dir.gsub('\\', '/')] = obj_dir.gsub('\\', '/')
83
+ end
84
+
85
+ # Return the file name to be built from source_fname with suffix suffix.
86
+ # This method takes into account the Environment's build directories.
87
+ # It also creates any parent directories needed to be able to open and
88
+ # write to the output file.
89
+ def get_build_fname(source_fname, suffix)
90
+ build_fname = source_fname.set_suffix(suffix).gsub('\\', '/')
91
+ @build_dirs.each do |src_dir, obj_dir|
92
+ build_fname.sub!(/^#{src_dir}\//, "#{obj_dir}/")
93
+ end
94
+ FileUtils.mkdir_p(File.dirname(build_fname))
95
+ build_fname
96
+ end
97
+
98
+ # Access a construction variable or environment option.
99
+ # @see VarSet#[]
100
+ def [](*args)
101
+ @varset.send(:[], *args)
102
+ end
103
+
104
+ # Set a construction variable or environment option.
105
+ # @see VarSet#[]=
106
+ def []=(*args)
107
+ @varset.send(:[]=, *args)
108
+ end
109
+
110
+ # Add a set of construction variables or environment options.
111
+ # @see VarSet#append
112
+ def append(*args)
113
+ @varset.send(:append, *args)
114
+ end
115
+
116
+ # Return a list of target file names
117
+ def targets
118
+ @targets.keys
119
+ end
120
+
121
+ # Return a list of sources needed to build target target.
122
+ def target_sources(target)
123
+ @targets[target][:source] rescue nil
124
+ end
125
+
126
+ # Build all target specified in the Environment.
127
+ # When a block is passed to Environment.new, this method is automatically
128
+ # called after the block returns.
129
+ def process
130
+ cache = Cache.new
131
+ targets_processed = Set.new
132
+ process_target = proc do |target|
133
+ if @targets[target][:source].map do |src|
134
+ targets_processed.include?(src) or not @targets.include?(src) or process_target.call(src)
135
+ end.all?
136
+ @targets[target][:builder].run(target,
137
+ @targets[target][:source],
138
+ cache,
139
+ self,
140
+ *@targets[target][:args])
141
+ else
142
+ false
143
+ end
144
+ end
145
+ @targets.each do |target, info|
146
+ next if targets_processed.include?(target)
147
+ unless process_target.call(target)
148
+ $stderr.puts "Error: failed to build #{target}"
149
+ break
150
+ end
151
+ end
152
+ cache.write
153
+ end
154
+
155
+ # Build a command line from the given template, resolving references to
156
+ # variables using the Environment's construction variables and any extra
157
+ # variables specified.
158
+ # @param command_template [Array] template for the command with variable
159
+ # references
160
+ # @param extra_vars [Hash, VarSet] extra variables to use in addition to
161
+ # (or replace) the Environment's construction variables when building
162
+ # the command
163
+ def build_command(command_template, extra_vars)
164
+ @varset.merge(extra_vars).expand_varref(command_template)
165
+ end
166
+
167
+ # Execute a builder command
168
+ # @param short_desc [String] Message to print if the Environment's :echo
169
+ # mode is set to :short
170
+ # @param command [Array] The command to execute.
171
+ # @param options [Hash] Optional options to pass to {Kernel#system}.
172
+ def execute(short_desc, command, options = {})
173
+ print_command = proc do
174
+ puts command.map { |c| c =~ /\s/ ? "'#{c}'" : c }.join(' ')
175
+ end
176
+ if @varset[:echo] == :command
177
+ print_command.call
178
+ elsif @varset[:echo] == :short
179
+ puts short_desc
180
+ end
181
+ system(*command, options).tap do |result|
182
+ unless result or @varset[:echo] == :command
183
+ $stdout.write "Failed command was: "
184
+ print_command.call
185
+ end
186
+ end
187
+ end
188
+
189
+ alias_method :orig_method_missing, :method_missing
190
+ def method_missing(method, *args)
191
+ if @builders.has_key?(method.to_s)
192
+ target, source, *rest = args
193
+ source = [source] unless source.is_a?(Array)
194
+ @targets[target] = {
195
+ builder: @builders[method.to_s],
196
+ source: source,
197
+ args: rest,
198
+ }
199
+ else
200
+ orig_method_missing(method, *args)
201
+ end
202
+ end
203
+
204
+ # Parse dependencies for a given target from a Makefile.
205
+ # This method is used internally by RScons builders.
206
+ # @param mf_fname [String] File name of the Makefile to read.
207
+ # @param target [String] Name of the target to gather dependencies for.
208
+ def parse_makefile_deps(mf_fname, target)
209
+ deps = []
210
+ buildup = ''
211
+ File.read(mf_fname).each_line do |line|
212
+ if line =~ /^(.*)\\\s*$/
213
+ buildup += ' ' + $1
214
+ else
215
+ if line =~ /^(.*): (.*)$/
216
+ target, tdeps = $1.strip, $2
217
+ if target == target
218
+ deps += tdeps.split(' ').map(&:strip)
219
+ end
220
+ end
221
+ buildup = ''
222
+ end
223
+ end
224
+ deps
225
+ end
226
+ end
227
+ end
@@ -0,0 +1,7 @@
1
+ # Standard Ruby Module class.
2
+ class Module
3
+ # @return the base module name (not the fully qualified name)
4
+ def short_name
5
+ name.split(':').last
6
+ end
7
+ end
@@ -0,0 +1,19 @@
1
+ # Standard Ruby String class.
2
+ class String
3
+ # Check if the given string ends with any of the supplied suffixes
4
+ # @param suffix [String, Array] The suffix to look for.
5
+ # @return a true value if the string ends with one of the suffixes given.
6
+ def has_suffix?(suffix)
7
+ if suffix
8
+ suffix = [suffix] if suffix.is_a?(String)
9
+ suffix.find {|s| self.end_with?(s)}
10
+ end
11
+ end
12
+
13
+ # Return a new string with the suffix (dot character and extension) changed
14
+ # to the given suffix.
15
+ # @param suffix [String] The new suffix.
16
+ def set_suffix(suffix = '')
17
+ sub(/\.[^.]*$/, suffix)
18
+ end
19
+ end
@@ -0,0 +1,83 @@
1
+ module Rscons
2
+ # This class represents a collection of variables which can be accessed
3
+ # as certain types
4
+ class VarSet
5
+ # The underlying hash
6
+ attr_reader :vars
7
+
8
+ # Create a VarSet
9
+ # @param vars [Hash] Optional initial variables.
10
+ def initialize(vars = {})
11
+ @vars = vars
12
+ end
13
+
14
+ # Access the value of variable as a particular type
15
+ # @param key [String, Symbol] The variable name.
16
+ # @param type [Symbol, nil] Optional specification of the type desired.
17
+ # If the variable is a String and type is :array, a 1-element array with
18
+ # the variable value will be returned. If the variable is an Array and
19
+ # type is :string, the first element from the variable value will be
20
+ # returned.
21
+ def [](key, type = nil)
22
+ val = @vars[key]
23
+ if type == :array and val.is_a?(String)
24
+ [val]
25
+ elsif type == :string and val.is_a?(Array)
26
+ val.first
27
+ else
28
+ val
29
+ end
30
+ end
31
+
32
+ # Assign a value to a variable.
33
+ # @param key [String, Symbol] The variable name.
34
+ # @param val [Object] The value.
35
+ def []=(key, val)
36
+ @vars[key] = val
37
+ end
38
+
39
+ # Add or overwrite a set of variables
40
+ # @param values [VarSet, Hash] New set of variables.
41
+ def append(values)
42
+ values = values.vars if values.is_a?(VarSet)
43
+ @vars.merge!(values)
44
+ self
45
+ end
46
+
47
+ # Create a new VarSet object based on the first merged with other.
48
+ # @param other [VarSet, Hash] Other variables to add or overwrite.
49
+ def merge(other = {})
50
+ VarSet.new(Marshal.load(Marshal.dump(@vars))).append(other)
51
+ end
52
+ alias_method :clone, :merge
53
+
54
+ # Replace "$" variable references in varref with the variables values,
55
+ # recursively.
56
+ # @param varref [String, Array] Value containing references to variables.
57
+ def expand_varref(varref)
58
+ if varref.is_a?(Array)
59
+ varref.map do |ent|
60
+ expand_varref(ent)
61
+ end.flatten
62
+ else
63
+ if varref =~ /^(.*)\$\[(\w+)\](.*)$/
64
+ # expand array with given prefix, suffix
65
+ prefix, varname, suffix = $1, $2, $3
66
+ varval = expand_varref(@vars[varname])
67
+ unless varval.is_a?(Array)
68
+ raise "Array expected for $#{varname}"
69
+ end
70
+ varval.map {|e| "#{prefix}#{e}#{suffix}"}
71
+ elsif varref =~ /^\$(.*)$/
72
+ # expand a single variable reference
73
+ varname = $1
74
+ varval = expand_varref(@vars[varname])
75
+ varval or raise "Could not find variable #{varname.inspect}"
76
+ expand_varref(varval)
77
+ else
78
+ varref
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,4 @@
1
+ module Rscons
2
+ # gem version
3
+ VERSION = "0.0.1"
4
+ end
data/rscons.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rscons/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rscons"
8
+ gem.version = Rscons::VERSION
9
+ gem.authors = ["Josh Holtrop"]
10
+ gem.email = ["jholtrop@gmail.com"]
11
+ gem.description = %q{Software construction library inspired by SCons and implemented in Ruby}
12
+ gem.summary = %q{Software construction library inspired by SCons and implemented in Ruby}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_development_dependency "rspec-core"
21
+ gem.add_development_dependency "rspec-mocks"
22
+ gem.add_development_dependency "rspec-expectations"
23
+ gem.add_development_dependency "rspec"
24
+ gem.add_development_dependency "rake"
25
+ gem.add_development_dependency "simplecov"
26
+ gem.add_development_dependency "json"
27
+ gem.add_development_dependency 'rdoc'
28
+ gem.add_development_dependency "yard"
29
+ end
@@ -0,0 +1,146 @@
1
+ require 'fileutils'
2
+
3
+ describe Rscons do
4
+ before(:all) do
5
+ FileUtils.rm_rf('build_tests_run')
6
+ @owd = Dir.pwd
7
+ end
8
+
9
+ after(:each) do
10
+ Dir.chdir(@owd)
11
+ FileUtils.rm_rf('build_tests_run')
12
+ end
13
+
14
+ def build_testdir
15
+ if File.exists?("build.rb")
16
+ system("ruby -I #{@owd}/lib -r rscons build.rb > build.out")
17
+ end
18
+ get_build_output
19
+ end
20
+
21
+ def test_dir(build_test_directory)
22
+ FileUtils.cp_r("build_tests/#{build_test_directory}", 'build_tests_run')
23
+ Dir.chdir("build_tests_run")
24
+ build_testdir
25
+ end
26
+
27
+ def file_sub(fname)
28
+ contents = File.read(fname)
29
+ replaced = ''
30
+ contents.each_line do |line|
31
+ replaced += yield(line)
32
+ end
33
+ File.open(fname, 'w') do |fh|
34
+ fh.write(replaced)
35
+ end
36
+ end
37
+
38
+ def get_build_output
39
+ File.read('build.out').lines.map(&:strip)
40
+ end
41
+
42
+ ###########################################################################
43
+ # Tests
44
+ ###########################################################################
45
+
46
+ it 'builds a C program with one source file' do
47
+ test_dir('simple')
48
+ File.exists?('simple.o').should be_true
49
+ `./simple`.should == "This is a simple C program\n"
50
+ end
51
+
52
+ it 'prints commands as they are executed' do
53
+ lines = test_dir('simple')
54
+ lines.should == [
55
+ 'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
56
+ 'gcc -o simple simple.o',
57
+ ]
58
+ end
59
+
60
+ it 'prints short representations of the commands being executed' do
61
+ lines = test_dir('header')
62
+ lines.should == [
63
+ 'CC header.o',
64
+ 'LD header',
65
+ ]
66
+ end
67
+
68
+ it 'builds a C program with one source file and one header file' do
69
+ test_dir('header')
70
+ File.exists?('header.o').should be_true
71
+ `./header`.should == "The value is 2\n"
72
+ end
73
+
74
+ it 'rebuilds a C module when a header it depends on changes' do
75
+ test_dir('header')
76
+ `./header`.should == "The value is 2\n"
77
+ file_sub('header.h') {|line| line.sub(/2/, '5')}
78
+ build_testdir
79
+ `./header`.should == "The value is 5\n"
80
+ end
81
+
82
+ it 'does not rebuild a C module when its dependencies have not changed' do
83
+ lines = test_dir('header')
84
+ `./header`.should == "The value is 2\n"
85
+ lines.should == [
86
+ 'CC header.o',
87
+ 'LD header',
88
+ ]
89
+ lines = build_testdir
90
+ lines.should == []
91
+ end
92
+
93
+ it "does not rebuild a C module when only the file's timestampe has changed" do
94
+ lines = test_dir('header')
95
+ `./header`.should == "The value is 2\n"
96
+ lines.should == [
97
+ 'CC header.o',
98
+ 'LD header',
99
+ ]
100
+ file_sub('header.c') {|line| line}
101
+ lines = build_testdir
102
+ lines.should == []
103
+ end
104
+
105
+ it 're-links a program when the link flags have changed' do
106
+ lines = test_dir('simple')
107
+ lines.should == [
108
+ 'gcc -c -o simple.o -MMD -MF simple.mf simple.c',
109
+ 'gcc -o simple simple.o',
110
+ ]
111
+ file_sub('build.rb') {|line| line.sub(/.*CHANGE.FLAGS.*/, ' env["LIBS"] += ["c"]')}
112
+ lines = build_testdir
113
+ lines.should == ['gcc -o simple simple.o -lc']
114
+ end
115
+
116
+ it 'builds object files in a different build directory' do
117
+ lines = test_dir('build_dir')
118
+ `./build_dir`.should == "Hello from two()\n"
119
+ File.exists?('build/one/one.o').should be_true
120
+ File.exists?('build/two/two.o').should be_true
121
+ end
122
+
123
+ it 'allows Ruby classes as custom builders to be used to construct files' do
124
+ lines = test_dir('custom_builder')
125
+ lines.should == ['CC program.o', 'LD program']
126
+ File.exists?('inc.h').should be_true
127
+ `./program`.should == "The value is 5678\n"
128
+ end
129
+
130
+ it 'allows cloning Environment objects' do
131
+ lines = test_dir('clone_env')
132
+ lines.should == [
133
+ %q{gcc -c -o debug/program.o -MMD -MF debug/program.mf '-DSTRING="Debug Version"' -O2 src/program.c},
134
+ %q{gcc -o program-debug debug/program.o},
135
+ %q{gcc -c -o release/program.o -MMD -MF release/program.mf '-DSTRING="Release Version"' -O2 src/program.c},
136
+ %q{gcc -o program-release release/program.o},
137
+ ]
138
+ end
139
+
140
+ it 'builds a C++ program with one source file' do
141
+ test_dir('simple_cc')
142
+ File.exists?('simple.o').should be_true
143
+ `./simple`.should == "This is a simple C++ program\n"
144
+ end
145
+
146
+ end