bake 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CONCEPTS ADDED
@@ -0,0 +1,2 @@
1
+ = Concepts
2
+
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
@@ -0,0 +1,2 @@
1
+ = Reference
2
+
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
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ begin
4
+ require 'rubygems'
5
+ rescue LoadError
6
+ # no rubygems to load, so we fail silently
7
+ end
8
+
9
+ require 'bake'
10
+
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
+