bake 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CONCEPTS +2 -0
- data/MIT-LICENSE +21 -0
- data/README +38 -0
- data/REFERENCE +2 -0
- data/TUTORIAL +133 -0
- data/bin/bake +10 -0
- data/lib/bake.rb +155 -0
- data/lib/bake/common_scheme.rb +42 -0
- data/lib/bake/configuration.rb +154 -0
- data/lib/bake/context.rb +101 -0
- data/lib/bake/cpp_scheme.rb +460 -0
- data/lib/bake/project_loader.rb +111 -0
- data/lib/bake/qt_scheme.rb +62 -0
- data/lib/bake/scheme.rb +104 -0
- data/lib/bake/scheme_loader.rb +28 -0
- data/lib/bake/string_utils.rb +19 -0
- data/lib/bake/target.rb +115 -0
- data/lib/bake/toolset.rb +11 -0
- data/lib/bake_version.rb +5 -0
- data/test/bake_test.rb +5 -0
- data/test/configuration_test.rb +102 -0
- data/test/context_test.rb +94 -0
- data/test/scheme_test.rb +121 -0
- data/test/target_test.rb +93 -0
- metadata +76 -0
data/CONCEPTS
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
Copyright (c) 2007 Dylan Trotter
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
data/README
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= Bake
|
2
|
+
|
3
|
+
Bake is a build automation utility somewhat akin to the Ruby tool Rake. Like Rake[http://rake.rubyforge.org/], Bake definition files (or <i>bakefiles</i>) are written in pure Ruby. The similarities between Rake and Bake pretty much end there, however. Bake generally takes a much higher level approach. Instead of defining tasks and specifying the commands that get executed by that task, you define high level products, such as C++ libraries or Java WAR files in your bakefiles. Thus bakefiles are simple, readable definitions of the final products of your project.
|
4
|
+
|
5
|
+
== Download
|
6
|
+
|
7
|
+
You can download the most recent version of Bake at here[http://rubyforge.org/frs/?group_id=3477]
|
8
|
+
|
9
|
+
== Installation
|
10
|
+
|
11
|
+
Currently, Bake only support Gem installs. Simply run:
|
12
|
+
|
13
|
+
% gem install bake
|
14
|
+
|
15
|
+
If you don't like how Gems taste, it's probably not difficult to set up Bake from source. Download one of the source distributions and let me know how it works out :)
|
16
|
+
|
17
|
+
== Further Reading
|
18
|
+
|
19
|
+
Tutorial Introduction:: TUTORIAL[link:files/TUTORIAL.html]
|
20
|
+
|
21
|
+
Concepts:: CONCEPTS[link:files/CONCEPTS.html]
|
22
|
+
|
23
|
+
Reference:: REFERENCE[link:files/REFERENCE.html]
|
24
|
+
|
25
|
+
== History
|
26
|
+
|
27
|
+
Bake took inspiration from the many great build tools that exist out there. In particular, the idea of using Ruby as a domain specific language for build systems was pioneered Jim Weirich in his Make-like utility, Rake. After working with Rake for some time, however, I found that it was difficult to maintain large projects effectively due to the fact that Rake is quite low-level.
|
28
|
+
|
29
|
+
Soon after I abandoned Rake, I found another great Ruby-based system called Rant created by Stefan Lang. Rant was slightly better and it had many great ideas for how a Ruby-based build system should work such as improved error messages, good support for larger multi-file projects, etc. I tried for a time to write Bake as extensions to Rant since I felt it a good starting point. In the end, however, it had similar limitations to Rake and I abandoned further work on Rant.
|
30
|
+
|
31
|
+
On the other end of the spectrum there's {Boost.Build}[http://www.boost.org/tools/build/jam_src/index.html] which is the build system used for the {Boost C++ Libraries}[http://boost.org/]. Boost.Build does have a reasonably good project-based approach, however, the syntax is abominable. It's based on {Perforce Jam}[http://www.perforce.com/jam/jam.html] which is quite possibly the most hideous build system ever conceived. Ugly doesn't begin to describe the syntax, nor incomprehensible the so-called Jamfiles.
|
32
|
+
|
33
|
+
The panacea is Bake. I think it satisfies my rather loose requirements: pretty, simple for simple projects, powerful for large projects and fast. The items in this list are at various stages in their evolution, but I think it solves my build problems better than any of the others I listed above.
|
34
|
+
|
35
|
+
== License
|
36
|
+
|
37
|
+
:include: MIT-LICENSE
|
38
|
+
|
data/REFERENCE
ADDED
data/TUTORIAL
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
= Tutorial Introduction
|
2
|
+
|
3
|
+
== A C++ Executable
|
4
|
+
|
5
|
+
=== Bakefiles and Targets
|
6
|
+
|
7
|
+
Once you've installed Bake, you should be able to execute it by running the <tt>bake</tt> command. This will cause Bake to load a file called <i>root.bake</i> in the current working directory or some ancestor directory of the current directory. This <b>bakefile</b> defines the contents of the project including any settings required to properly make the build targets. Let's see the contents of <i>root.bake</i> for a simple C++ project consisting of a single source file, <i>main.cpp</i>, which gets compiled into an executable called <i>test</i>:
|
8
|
+
|
9
|
+
# root.bake
|
10
|
+
exe 'test' do
|
11
|
+
src 'main.cpp'
|
12
|
+
end
|
13
|
+
|
14
|
+
The <tt>exe</tt> command specifies that we'd like to create an executable <b>target</b>. A target is any product that gets created as a result of the build process. There are many kinds of targets, such as <tt>lib</tt>s, or C++ libraries, etc. To make this example a little more concrete, let's also create <i>main.cpp</i>:
|
15
|
+
|
16
|
+
// main.cpp
|
17
|
+
#include <iostream>
|
18
|
+
int main()
|
19
|
+
{
|
20
|
+
std::cout << "hello bake" << std::endl;
|
21
|
+
return 0;
|
22
|
+
}
|
23
|
+
|
24
|
+
Both these files should be in the same directory, call it <i>test/</i>. The layout is as follows:
|
25
|
+
|
26
|
+
test/
|
27
|
+
root.bake
|
28
|
+
main.cpp
|
29
|
+
|
30
|
+
If you run <tt>bake</tt> from the <i>test/</i> directory you should see output resembling "<tt>root.bake:1: undefined symbol 'exe'</tt>". The reason for this is that we have not specified what <b>tooset</b> we will be using to build C++ targets. Bake supports a number of C++ toolsets (consult the Reference for full details) and each toolset should treat our test project identically, so which toolset you use should not matter. For the purposes of this example, I'll use the <tt>gcc</tt> toolset.
|
31
|
+
|
32
|
+
=== The <tt>using</tt> Directive
|
33
|
+
|
34
|
+
Generally, it is a good idea to keep your project definitions separate from which toolset you're using since your choice of toolset is conceptually distinct from your project layout. This is especially true from cross-platform projects. For this reason, in this project we'll keep this information in a separate bakefile called <i>config.bake</i> which will also live in the <i>test/</i> directory:
|
35
|
+
|
36
|
+
# config.bake
|
37
|
+
using 'cpp.gcc'
|
38
|
+
|
39
|
+
The <tt>using</tt> directive above tells Bake that when building C++ targets, it should use the <tt>gcc</tt> toolset. If you now run the <tt>bake</tt> command, and as long as your toolset is properly configured, you should get an executable file called <i>test</i> (or <i>test.exe</i> on Windows platforms) in the <i>test/</i> directory.
|
40
|
+
|
41
|
+
=== Properties
|
42
|
+
|
43
|
+
Unfortunately, the <i>test</i> executable will probably be accompanied by a number of by-products of the build procedure and so our <i>test/</i> directory is now getting cluttered. It is common to keep products of the build in a separate output directory so that they don't get mixed up with our source files. The output directory is just one of a number of options supported by <tt>exe</tt> targets. To specify an output directory we set the <tt>:outdir</tt> <b>property</b> on <tt>test</tt>. Properties are just key/value pairs that are used by the toolset to output the desired product. The simplest way to set a property is to use the <tt>opt</tt> command on the <tt>test</tt> target, like this:
|
44
|
+
|
45
|
+
# root.bake
|
46
|
+
|
47
|
+
exe 'test' do
|
48
|
+
opt :outdir => 'bin'
|
49
|
+
|
50
|
+
src 'main.cpp'
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
Note that property names are <b>symbols</b> preceded by a colon (:) whereas, in this case, the property value is a <b>string</b>. Property values can be any of a number of different types such as strings, booleans, numbers, etc.
|
55
|
+
|
56
|
+
Delete all the product files generated by the last build so that your directory structure looks like:
|
57
|
+
|
58
|
+
test/
|
59
|
+
config.bake
|
60
|
+
|
61
|
+
root.bake
|
62
|
+
|
63
|
+
main.cpp
|
64
|
+
|
65
|
+
Now run Bake again. Note that a new directory called <i>bin/</i> has been created and that all the products of the build have been placed inside. C++ executable targets have a number of other properties that affect different aspects of the build. For example, to turn off multithreading for a single threaded application, you would specify "<tt>opt :multithreaded? => false</tt>". Note that we've used a boolean value type instead of a string. The type is important as a value of <tt>'false'</tt> would result in the opposite behavior we would expect (this is because strings always evaluate to true when examined in a boolean context). For a full list of target properties and their types, see the Reference.
|
66
|
+
|
67
|
+
== A More Complicated Example
|
68
|
+
|
69
|
+
=== Multiple Targets, One Bakefile
|
70
|
+
|
71
|
+
So far we've seen Bake build a simple C++ executable, and it did the job pretty well. Bake's strengths, however, are only seen when we have more complicated projects to test it with. Bake is designed with large projects in mind and it makes managing these projects a snap.
|
72
|
+
|
73
|
+
To see how Bake handles multi-directory projects, let's create a project composed of a C++ library and an executable that depends on the library. Let's try the following directory structure:
|
74
|
+
|
75
|
+
test2/
|
76
|
+
test_lib/
|
77
|
+
source1.cpp
|
78
|
+
source2.cpp
|
79
|
+
test_exe/
|
80
|
+
source1.cpp
|
81
|
+
|
82
|
+
As a first try at managing this project, let's create a <i>root.bake</i> in the <i>test2</i> directory that contains the definitions for both <tt>test_lib</tt> and <tt>test_exe</tt>:
|
83
|
+
|
84
|
+
# root.bake
|
85
|
+
lib 'test_lib' do
|
86
|
+
src 'test_lib/source1.cpp', 'test_lib/source2.cpp'
|
87
|
+
end
|
88
|
+
|
89
|
+
exe 'test_exe' do
|
90
|
+
dep 'test_lib'
|
91
|
+
src 'test_exe/source1.cpp'
|
92
|
+
end
|
93
|
+
|
94
|
+
Here we have created a <tt>lib</tt> target called <tt>test_lib</tt> containing two source files, and an executable file that depends on the library and another source file. This is pretty simple and will probably seem to work, however, there's an insidious bug waiting to cause trouble when you least expect it. Note that there are two files called <i>source1.cpp</i>. In each case, a C++ object file called <i>source1.o</i> (or <i>source1.obj</i> on Windows) will be created in the <i>test2/</i> directory, the object file from <tt>test_exe</tt> will overwrite the object file from <tt>test_lib</tt>. This will not immediately cause a problem, since the library object file was added to the library before the executable object file was created but it will almost certainly cause problems later on.
|
95
|
+
|
96
|
+
=== More About Properties
|
97
|
+
|
98
|
+
The easiest way to avoid conflicts like these is to set the <tt>:outdir</tt> property differently on each target so that the object files will live in different directories. To do this, however, we're going to take a shortcut and only set the output directory once:
|
99
|
+
|
100
|
+
# root.bake
|
101
|
+
opt :outdir => 'bin/${name}'
|
102
|
+
|
103
|
+
|
104
|
+
lib 'test_lib' do
|
105
|
+
|
106
|
+
src 'test_lib/source1.cpp', 'test_lib/source2.cpp'
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
|
111
|
+
|
112
|
+
exe 'test_exe' do
|
113
|
+
|
114
|
+
dep 'test_lib'
|
115
|
+
|
116
|
+
src 'test_exe/source1.cpp'
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
If you run Bake now, the outputs from <tt>test_lib</tt> will end up in <i>test2/bin/test_lib/</i> and the outputs from <tt>test_exe</tt> will end up in <i>test2/bin/test_exe</i>. If you're confused, don't worry, we've taken two steps forward here. Not only is the value of the <tt>:outdir</tt> property not explicitly set on either target, but it's out in no-man's land, what gives? Furthermore, what's up with the <tt>${projname}</tt> business?
|
121
|
+
|
122
|
+
The answer to the first question is related to an important fact about targets: they are tree-like. Every target (except the root target, which we'll get into later) has a single parent target, and many targets have child targets. So what target is the parent of <tt>test_lib</tt> and <tt>test_exe</tt>? An implicit <b>project</b> target that is created when <i>root.bake</i> is processed. Therefore, the <tt>opt</tt> command is being executed on the implicit project target and so <tt>:outdir</tt> is set for the project.
|
123
|
+
|
124
|
+
Furthermore, properties are inherited. So if a property is explicitly defined for a parent target but is not explicitly defined for the child, then the property for the child is the same as that for the parent. Properties can be overridden by explicitly using the <tt>opt</tt> command on the child, in which case the parent's property value will be ignored.
|
125
|
+
|
126
|
+
So in the example above, the <tt>:outdir</tt> property is inherited by the <tt>test_lib</tt> and <tt>test_exe</tt> targets as <tt>'bin/${projname}'</tt>.
|
127
|
+
|
128
|
+
The answer to the second question is even simpler. String property values are dynamic. Any <tt>${...}</tt> tokens found in the string will be evaluated when the property is queried. In addition, it will be queried in the context of the target being queried, not the target where the property was originally defined. So when the GCC toolset builds <tt>test_lib</tt> and <tt>test_exe</tt>, it will query the <tt>:outdir</tt> property on each target and it will evaluate to <tt>'bin/test_lib'</tt> and <tt>'bin/test_exe'</tt> respectively.
|
129
|
+
|
130
|
+
=== Modular Bakefiles
|
131
|
+
|
132
|
+
TODO
|
133
|
+
|
data/bin/bake
ADDED
data/lib/bake.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
require 'bake/scheme_loader'
|
2
|
+
require 'bake/context'
|
3
|
+
require 'bake/project_loader'
|
4
|
+
require 'bake_version'
|
5
|
+
require 'optparse'
|
6
|
+
|
7
|
+
module Bake
|
8
|
+
class CommandLineParseError < RuntimeError
|
9
|
+
end
|
10
|
+
|
11
|
+
class ProgramInfoRequested < RuntimeError
|
12
|
+
end
|
13
|
+
|
14
|
+
class App
|
15
|
+
@@instance = nil
|
16
|
+
|
17
|
+
def self.instance
|
18
|
+
return @@instance
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
raise "App instance already created" if @@instance
|
23
|
+
@@instance = self
|
24
|
+
@scheme_loader = SchemeLoader.new
|
25
|
+
@context = Context.new(@scheme_loader)
|
26
|
+
end
|
27
|
+
|
28
|
+
def run(*args)
|
29
|
+
options, args = parse_command_line(*args)
|
30
|
+
props, targets = process_excess_args(*args)
|
31
|
+
props[:platform] = determine_platform
|
32
|
+
|
33
|
+
@project_loader = ProjectLoader.new(@context, Dir.pwd, props)
|
34
|
+
targets = targets.collect do |name|
|
35
|
+
target = @project_loader.invok_project.find(name)
|
36
|
+
if !target
|
37
|
+
raise CommandLineParseError,
|
38
|
+
"could not find target '#{name}'"
|
39
|
+
end
|
40
|
+
target
|
41
|
+
end
|
42
|
+
|
43
|
+
method = options[:clean] ? :clean : :build
|
44
|
+
if targets.empty?
|
45
|
+
send(method, @project_loader.invok_project)
|
46
|
+
else
|
47
|
+
targets.each { |target| send(method, target) }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
def build(target)
|
53
|
+
return if target[:built?]
|
54
|
+
target.deps.each { |dep| build(dep) }
|
55
|
+
target.children.each { |child| build(child) }
|
56
|
+
toolset = toolset(target)
|
57
|
+
Dir.chdir(target[:cwdir]) do
|
58
|
+
toolset.build(target) if toolset
|
59
|
+
end
|
60
|
+
target.opt :built? => true
|
61
|
+
end
|
62
|
+
|
63
|
+
def clean(target)
|
64
|
+
target.children.each do |child|
|
65
|
+
clean(child)
|
66
|
+
end
|
67
|
+
toolset = toolset(target)
|
68
|
+
toolset.clean(target) if toolset
|
69
|
+
end
|
70
|
+
|
71
|
+
def toolset(target)
|
72
|
+
if !target.class.const_defined?(:SCHEME)
|
73
|
+
raise "no toolset for target '#{target.class.name}'"
|
74
|
+
end
|
75
|
+
scheme = target.class.const_get(:SCHEME)
|
76
|
+
return scheme.toolset if scheme
|
77
|
+
return nil
|
78
|
+
end
|
79
|
+
|
80
|
+
def determine_platform
|
81
|
+
if RUBY_PLATFORM =~ /darwin/i
|
82
|
+
return 'osx'
|
83
|
+
elsif RUBY_PLATFORM =~ /linux/i
|
84
|
+
return 'linux'
|
85
|
+
elsif RUBY_PLATFORM =~ /mswin/i
|
86
|
+
return 'win32'
|
87
|
+
end
|
88
|
+
return 'other'
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_command_line(*args)
|
92
|
+
options = {}
|
93
|
+
opts = OptionParser.new do |opts|
|
94
|
+
opts.banner = "Usage: bake [options] [target ...]"
|
95
|
+
opts.on('--clean', 'Clean the given targets') do |clean|
|
96
|
+
options[:clean] = clean
|
97
|
+
end
|
98
|
+
opts.on_tail('-h', '--help', 'Show usage') do
|
99
|
+
raise ProgramInfoRequested, opts
|
100
|
+
end
|
101
|
+
opts.on_tail('--version', 'Show version information') do
|
102
|
+
raise ProgramInfoRequested, 'bake ' + VERSION_STRING
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
begin
|
107
|
+
opts.parse!(args)
|
108
|
+
rescue ProgramInfoRequested
|
109
|
+
raise
|
110
|
+
rescue Exception => e
|
111
|
+
raise CommandLineParseError,
|
112
|
+
"#{e.message} (use --help for usage information)"
|
113
|
+
end
|
114
|
+
return options, args
|
115
|
+
end
|
116
|
+
|
117
|
+
def process_excess_args(*args)
|
118
|
+
props = {}
|
119
|
+
targets = []
|
120
|
+
args.each do |name|
|
121
|
+
if name.count('=') == 1
|
122
|
+
key, val = name.split('=')
|
123
|
+
props[key.to_sym] = val
|
124
|
+
else
|
125
|
+
targets << name
|
126
|
+
end
|
127
|
+
end
|
128
|
+
return props, targets
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def parse_backtrace(backtrace)
|
134
|
+
if backtrace.find { |entry| entry =~ /^.*?(config|root|sub)\.bake:\d+/ }
|
135
|
+
return $&
|
136
|
+
end
|
137
|
+
return 'internal error'
|
138
|
+
end
|
139
|
+
|
140
|
+
exit_code = 1
|
141
|
+
begin
|
142
|
+
app = Bake::App.new
|
143
|
+
app.run(*ARGV)
|
144
|
+
exit_code = 0
|
145
|
+
rescue Bake::ProgramInfoRequested => e
|
146
|
+
puts e.message
|
147
|
+
exit_code = 0
|
148
|
+
rescue Bake::CommandLineParseError => e
|
149
|
+
puts e.message
|
150
|
+
rescue Exception => e
|
151
|
+
puts parse_backtrace(e.backtrace) + ': ' + e.message
|
152
|
+
end
|
153
|
+
|
154
|
+
exit(exit_code)
|
155
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'bake/toolset'
|
2
|
+
require 'bake/target'
|
3
|
+
|
4
|
+
module Common
|
5
|
+
class Default < Bake::Toolset
|
6
|
+
def build(target)
|
7
|
+
if target.is_a?(Directory)
|
8
|
+
make_dir(target)
|
9
|
+
elsif target.is_a?(FileClone)
|
10
|
+
copy(target)
|
11
|
+
else
|
12
|
+
raise "unrecognized target of class '#{target.class}'"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def clean(target)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def make_dir(target)
|
21
|
+
puts "building directory '#{target.name}'"
|
22
|
+
end
|
23
|
+
|
24
|
+
def copy(target)
|
25
|
+
puts "building copy '#{target.name}'"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Directory < Bake::Target
|
30
|
+
ACCESSORS = :dir
|
31
|
+
|
32
|
+
def initialize(name, parent)
|
33
|
+
super(parent)
|
34
|
+
@name = name
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class FileClone < Bake::Target
|
39
|
+
ACCESSORS = [ :copy, :clone ]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Bake
|
2
|
+
class PropertyNotFoundException < RuntimeError
|
3
|
+
end
|
4
|
+
|
5
|
+
class TemplateEvaluationException < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
module Configuration
|
9
|
+
def has_prop?(key)
|
10
|
+
cfg = self
|
11
|
+
while cfg
|
12
|
+
return true if cfg.has_prop_impl(key)
|
13
|
+
cfg = cfg.parent
|
14
|
+
end
|
15
|
+
return false
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
return get(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(key)
|
23
|
+
if multival(key)
|
24
|
+
cfg = self
|
25
|
+
opts = []
|
26
|
+
while cfg
|
27
|
+
vals = cfg.options[key]
|
28
|
+
opts.concat(vals.collect { |val| value_of(val) }) if vals
|
29
|
+
cfg = cfg.parent
|
30
|
+
end
|
31
|
+
return opts
|
32
|
+
else
|
33
|
+
cfg = self
|
34
|
+
opt = default = nil
|
35
|
+
found_opt = found_default = false
|
36
|
+
while cfg
|
37
|
+
if cfg.requirements.has_key?(key)
|
38
|
+
return value_of(cfg.requirements[key])
|
39
|
+
end
|
40
|
+
if !found_opt
|
41
|
+
if cfg.options.has_key?(key)
|
42
|
+
opt = cfg.options[key]
|
43
|
+
found_opt = true
|
44
|
+
elsif !found_default && cfg.defaults.has_key?(key)
|
45
|
+
default = cfg.defaults[key]
|
46
|
+
found_default = true
|
47
|
+
end
|
48
|
+
end
|
49
|
+
cfg = cfg.parent
|
50
|
+
end
|
51
|
+
if found_opt
|
52
|
+
return value_of(opt)
|
53
|
+
elsif found_default
|
54
|
+
return value_of(default)
|
55
|
+
end
|
56
|
+
raise PropertyNotFoundException, "no such property '#{key}'"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def is?(prop)
|
61
|
+
key, val = key_val(prop)
|
62
|
+
raise 'no multivalue comparisons' if multival(key)
|
63
|
+
begin
|
64
|
+
return get(key) == val
|
65
|
+
rescue PropertyNotFoundException
|
66
|
+
return false
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def opt(prop)
|
71
|
+
key, val = key_val(prop)
|
72
|
+
if multival(key)
|
73
|
+
vals = options[key] ||= []
|
74
|
+
if val.respond_to?(:to_ary)
|
75
|
+
vals.concat(val)
|
76
|
+
else
|
77
|
+
vals << val
|
78
|
+
end
|
79
|
+
else
|
80
|
+
options[key] = val
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def req(prop)
|
85
|
+
key, val = key_val(prop)
|
86
|
+
raise 'no multivalue requirements' if multival(key)
|
87
|
+
cfg = self
|
88
|
+
while cfg
|
89
|
+
req = cfg.requirements[key]
|
90
|
+
if req
|
91
|
+
raise 'conflicting requirements' if req != val
|
92
|
+
break
|
93
|
+
end
|
94
|
+
cfg = cfg.parent
|
95
|
+
end
|
96
|
+
requirements[key] = val
|
97
|
+
end
|
98
|
+
|
99
|
+
def default(prop)
|
100
|
+
key, val = key_val(prop)
|
101
|
+
raise 'no multivalue defaults' if multival(key)
|
102
|
+
defaults[key] = val
|
103
|
+
end
|
104
|
+
|
105
|
+
protected
|
106
|
+
def options
|
107
|
+
return @options ||= {}
|
108
|
+
end
|
109
|
+
|
110
|
+
def requirements
|
111
|
+
return @requirements ||= {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def defaults
|
115
|
+
return @defaults ||= {}
|
116
|
+
end
|
117
|
+
|
118
|
+
def has_prop_impl(key)
|
119
|
+
return requirements.has_key?(key) ||
|
120
|
+
options.has_key?(key) ||
|
121
|
+
defaults.has_key?(key)
|
122
|
+
end
|
123
|
+
|
124
|
+
def key_val(prop)
|
125
|
+
raise 'invalid args' if !prop.respond_to?(:to_hash)
|
126
|
+
prop = prop.to_hash
|
127
|
+
raise 'bad' if prop.size != 1
|
128
|
+
key = prop.keys[0]
|
129
|
+
return [ key, prop[key] ]
|
130
|
+
end
|
131
|
+
|
132
|
+
def multival(key)
|
133
|
+
return key.to_s[-1, 1] == 's'
|
134
|
+
end
|
135
|
+
|
136
|
+
def value_of(val)
|
137
|
+
if val.instance_of?(String)
|
138
|
+
return val.gsub(/\$\{(.*?)\}/) do
|
139
|
+
name = $1
|
140
|
+
if has_prop?(name.to_sym)
|
141
|
+
get(name.to_sym)
|
142
|
+
elsif ENV.has_key?(name)
|
143
|
+
ENV[name]
|
144
|
+
else
|
145
|
+
raise TemplateEvaluationException,
|
146
|
+
"no property or environment variable '#{name}'"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
return val
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|