buildr 0.15.0 → 0.16.0

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.
@@ -1,12 +1,3 @@
1
- # returning(obj) and with(obj)
2
- require "facet/kernel/with"
3
- # &:symbol goodness.
4
- require "facet/symbol/to_proc"
5
- # blank? on string and nil
6
- require "facet/string/blank"
7
- require "facet/nilclass/blank"
8
- # What it says.
9
- require "facet/module/alias_method_chain"
10
1
  require "highline"
11
2
 
12
3
 
@@ -1,42 +1,68 @@
1
1
  module Buildr
2
2
 
3
- # A project is a means to assemble multiple related tasks.
3
+ # A project is a convenient mechanism for managing all the tasks
4
+ # related to a given project. For complex applications, you may have
5
+ # several projects, or sub-projects for each of the modules.
4
6
  #
5
- # A project will create its own set of internal tasks, such as
6
- # compile, build, clean, install. These tasks are fed with project
7
- # properties, e.g. the clean task will remove the project.target_dir
8
- # directory, where files are created during the build process.
7
+ # A project definition creates its own set of tasks, prefixed with
8
+ # the project name. For example, each project has a clean, build
9
+ # and deploy task. For project +foo+ the task names are +foo:clean+,
10
+ # +foo:build+ and +foo:deploy+.
9
11
  #
10
- # Projects can be organized hierarchically such that sub-projects
11
- # inherit default properties from their parent project, and
12
- # participate in tasks executed on the parent project.
12
+ # Projects have properties, some of which they inherit from their
13
+ # parent project. Built it tasks use these properties, for example,
14
+ # the +clean+ task will remove the target directory specified by
15
+ # the +target_dir+ property. The +compile+ tasks uses the compiler
16
+ # option: you can set these options on the parent project and they
17
+ # will be inherited by all sub-projects.
18
+ #
19
+ # You can only define a project once using #define. You can obtain
20
+ # the project using #project. Note that, if you're obtain the project
21
+ # object before the project is defined, the values of the project
22
+ # properties are not set yet, neither are any of the default tasks.
23
+ # However, many tasks perform late binding, and so you can pass a
24
+ # project before defining it. For example, the +compile+ task can
25
+ # take a project definition and use it as a classpath dependency.
13
26
  #
14
27
  # For example:
15
- # define "parent" do |project|
16
- # project.version = "1.1"
28
+ # define "project1" do
29
+ # self.version = "1.1"
17
30
  #
18
- # define "child1"
19
- # define "child2"
31
+ # define "module1" do
32
+ # package :jar
33
+ # end
34
+ #
35
+ # define "module2" do
36
+ # compile.with project("project1:module1")
37
+ # package :jar
38
+ # end
20
39
  # end
21
40
  #
22
- # This definition will work for a directory structure of:
23
- # . -- Parent project
24
- # |__child 1
25
- # |__child 2
41
+ # project("project1").projects.map(&:name)
42
+ # => [ "project1:module1", "project1:module2" ]
43
+ # project("project1:module1").parent.name
44
+ # => "project1"
45
+ # project("project1:module1").version
46
+ # => "1.1"
47
+ #
48
+ # Each project has a base directory (see #base_dir). By default,
49
+ # a top-level project uses the current directory, and each sub-project
50
+ # uses a sub-directory relative to the parent project.
26
51
  #
27
- # The sub-projects child1 and child2 inherit the project version number
28
- # from the parent project. In addition, for certain tasks such as build,
29
- # running the task on the parent project will also run it on all
30
- # sub-projects.
52
+ # For the above example, the directory structure is:
53
+ # .
54
+ # |__module1
55
+ # |__module2
31
56
  #
32
- # Use the #define method to define a new project, and within the context
33
- # of a project to define a sub-project. Use the #project method to find
34
- # an existing project, and within the context of a project, one of its
35
- # sub-projects.
57
+ # The project definition tasks a block and yields by passing the project
58
+ # definition. For convenience, the block is also executed in the context
59
+ # of the project object, as if with instance_eval.
36
60
  #
37
- # The main block in the project definition is executed during the project
38
- # definition. Use it to set project properties, and configure tasks.
39
- # Do not do any actual work in there.
61
+ # The following two are equivalent:
62
+ # define "project1" do |project|
63
+ # project.version = "1.1"
64
+ # self.version = "1.1"
65
+ # end
40
66
  class Project
41
67
 
42
68
  class << self
@@ -44,10 +70,11 @@ module Buildr
44
70
  # See Buildr#define.
45
71
  def define(*args, &block)
46
72
  name, properties = name_and_properties_from_args(*args)
47
- project(name).send :_define, properties, &block
73
+ raise "The name #{name} means sub-project #{name.split(':').last} belonging to the parent project #{name.split(':')[0..-2]}, and you can only define a sub-project inside the parent project" if name =~ /:/
74
+ project(name)._define properties, &block
48
75
  end
49
76
 
50
- # Returns the specified project (top-level only).
77
+ # See Buildr#project.
51
78
  def project(name)
52
79
  @projects ||= {}
53
80
  name.split(":").inject(nil) do |parent, name|
@@ -59,9 +86,9 @@ module Buildr
59
86
  end
60
87
  end
61
88
 
62
- # Returns all project definitions, including sub-projects.
89
+ # See Buildr#projects.
63
90
  def projects()
64
- (@projects || []).map { |name, project| project }
91
+ (@projects ||= {}).map { |name, project| project }.select(&:defined?).sort_by(&:name)
65
92
  end
66
93
 
67
94
  # Discard all project definitions.
@@ -69,18 +96,24 @@ module Buildr
69
96
  @projects.clear if @projects
70
97
  end
71
98
 
72
- # The Project class defines very little behavior for new process instances.
73
- # Use #on_create to add behavior to new process instances.
99
+ # The Project class defines minimal behavior for new projects.
100
+ # Use #on_define to add behavior when defining new projects.
101
+ # Whenever a new project is defined, it will yield to the block
102
+ # with the project object.
74
103
  #
75
- # Every block you register with #on_create will be called with a project
76
- # instance whenever a new project is created. You can then define tasks,
77
- # set project properties, etc.
104
+ # For example:
105
+ # # Set the default version of each project to "1.0".
106
+ # Project.on_define do |project|
107
+ # project.version ||= "1.0"
108
+ # end
78
109
  #
79
- # Keep in mind that the order on which #on_create blocks are called is
80
- # not determined. You cannot depend on tasks defined by another block to
81
- # exist at the time your block is called. Use Project#enhance for that.
82
- def on_create(&block)
83
- (@on_create ||= []) << block if block
110
+ # Keep in mind that the order in which #on_define blocks are
111
+ # called is not determined. You cannot depend on a previous
112
+ # #on_define to set properties or create new tasks. You would
113
+ # want to use the #after_block method instead, by calling it
114
+ # from within #after_block.
115
+ def on_define(&block)
116
+ (@on_define ||= []) << block if block
84
117
  end
85
118
 
86
119
  # :nodoc:
@@ -96,23 +129,45 @@ module Buildr
96
129
  name = properties.delete(:name)
97
130
  end
98
131
  raise ArgumentError, "Expected project name followed by (optional) project properties." unless args.empty?
99
- raise ArgumentError, "Missing project name." unless name
132
+ raise ArgumentError, "Missing project name, this is the first argument to the define method" unless name
100
133
  [ name, properties ]
101
134
  end
135
+
136
+ # :nodoc:
137
+ def warnings()
138
+ returning([]) do |msgs|
139
+ msgs << "There are no project definitions in your Rakefile" if @projects.nil? || @projects.empty?
140
+ # Find all projects that:
141
+ # * Are referenced but never defined. This is probably a typo.
142
+ # * Do not have a base directory.
143
+ (@projects || {}).each do |name, project|
144
+ msgs << "Project #{name} is referenced but not defined; you probably have a typo somewhere" unless project.defined?
145
+ msgs << "Project #{name} refers to the directory #{project.base_dir}, which does not exist" unless File.exist?(project.base_dir)
146
+ end
147
+ end
148
+ end
149
+
102
150
  end
103
151
 
104
152
  include Attributes
105
153
 
106
- # The project name.
154
+ # The project name. If this is a sub-project, it will be prefixed
155
+ # by the parent project's name. For example, "foo" and "foo:bar".
107
156
  attr_reader :name
108
157
 
109
158
  # The parent project if this is a sub-project.
110
159
  attr_reader :parent
111
160
 
112
- # The base directory of this project.
161
+ # The base directory of this project. The default for a top-level project
162
+ # is the same directory that holds the Rakefile. The default for a
163
+ # sub-project is a child directory with the same name.
164
+ #
165
+ # A project definition can change the base directory using the base_dir
166
+ # hash value. Be advised that the base directory and all values that
167
+ # depend on it can only be determined after the project is defined.
113
168
  attr_reader :base_dir
114
169
 
115
- # Always construct a project using Object#project or Project#project.
170
+ # :nodoc:
116
171
  def initialize(name, parent)
117
172
  fail "Missing project name" unless name
118
173
  @name = parent ? "#{parent.name}:#{name}" : name
@@ -125,123 +180,134 @@ module Buildr
125
180
  # For top-level project, a good default is the directory where we found the Rakefile.
126
181
  @base_dir = Dir.pwd
127
182
  end
128
- @actions = []
183
+ @after_block = []
184
+ @defined = false
129
185
  end
130
186
 
131
187
  # Define a new sub-project within this project.
132
188
  def define(*args, &block)
133
189
  name, properties = Project.name_and_properties_from_args(*args)
134
- Project.define("#{self.name}:#{name}", properties, &block)
190
+ project("#{self.name}:#{name}")._define properties, &block
135
191
  end
136
192
 
193
+ # Returns true if the project was already defined.
137
194
  def defined?()
138
195
  @defined
139
196
  end
140
197
 
141
- # Returns a path made from the specified arguments. Relative paths are turned
142
- # into absolute paths using the based directory of this project.
198
+ def to_s()
199
+ name
200
+ end
201
+
202
+ # Returns a path made from multiple arguments. Relative paths are turned into
203
+ # absolute paths using this project's base directory.
143
204
  #
144
- # If you pass multiple arguments, they are combined into a path using File#join.
145
- # If one of the arguments is a symbol, it is used to retrieve that process
146
- # property.
205
+ # Symbol arguments are converted to paths by calling the attribute accessor
206
+ # on the project. For example:
147
207
  #
148
208
  # For example:
149
209
  # path_to("foo", "bar")
210
+ # => /projects/project1/foo/bar
150
211
  # path_to(:target_dir, "foo")
212
+ # => /projects/project1/target/foo
151
213
  # path_to("/tmp")
152
- # are equivalent to:
153
- # File.join(base_dir, "foo", "bar")
154
- # File.join(base_dir, project.target_dir, "foo")
155
- # "/tmp"
214
+ # => /tmp
156
215
  def path_to(*args)
157
216
  File.expand_path(File.join(args.map { |arg| Symbol === arg ? send(arg) : arg.to_s }), base_dir)
158
217
  end
159
218
 
160
- # Returns the specified sub-project of this project, or any of its descendant,
161
- # or a top-level project.
162
- #
163
- # For example:
164
- # bar.project("baz")
165
- # will find the first match from:
166
- # foo:bar:baz
167
- # foo:baz
168
- # baz
219
+ # Same as Buildr#project.
169
220
  def project(name)
170
221
  Project.project(name)
171
222
  end
172
223
 
173
- # Returns all sub-projects defined in this project, including their
174
- # sub-projects.
224
+ # Same as Buildr#projects.
175
225
  def projects()
176
- prefix = name + ":"
177
- Project.projects.map { |project| project.starts_with?(prefix) }
226
+ Project.projects
178
227
  end
179
228
 
180
- # The project ID is the project name, and for a sub-project the
181
- # parent project ID followed by the project name, separated with a
182
- # hyphen. For example, "foo" and "foo-bar".
183
- def id()
184
- name.gsub(":", "-")
229
+ def sub_projects()
230
+ prefix = name + ":"
231
+ Project.projects.select { |project| project.name.starts_with?(prefix) }.sort_by(&:name)
185
232
  end
186
233
 
187
234
  # Define a recursive task.
188
235
  #
189
- # A recursive task for a project will execute all sub-project tasks of
190
- # the same name before it executes itself. In addition, if a task with
191
- # the same name exists (not prefixed with a project), it will execute
192
- # same task but only for the current project -- as determined by the
193
- # current working directory.
236
+ # A recursive task executes the task with the same name in the project,
237
+ # and in all its sub-projects. In fact, a recursive task actually adds
238
+ # itself as a prerequisite on the parent task.
194
239
  #
195
240
  # For example:
241
+ # define "foo" do
242
+ # define "bar" do
243
+ # define "baz" do
244
+ # end
245
+ # end
246
+ # end
247
+ #
196
248
  # rake foo:build
197
249
  # Will execute foo:build, foo:bar:build and foo:baz:build
198
250
  #
199
251
  # Inside the bar directory:
200
252
  # rake build
201
- # Will execute foo:bar:build.
253
+ # Will execute foo:bar:build and foo:baz:build.
202
254
  #
203
- # Note, this method is used to define the task as recursive. It will
204
- # also define the task if the task does not already exist. However,
205
- # you can define the task before calling #recursive_task, e.g. to
206
- # create a file task, or any other special purpose task.
255
+ # This method defines a RakeTask. If you need a different type of task,
256
+ # define the task first and then call #recursive_task.
207
257
  def recursive_task(arg, &block)
208
258
  name = Hash === arg ? arg.keys.first : arg
209
259
  returning(task(arg)) do |task|
210
260
  if parent
211
- Rake::Task["^#{parent.name}:#{name}"].enhance([ task ])
261
+ Rake::Task["^#{name}"].enhance([ task ])
212
262
  end
213
263
  task.enhance &block
214
264
  end
215
265
  end
216
266
 
217
- def enhance(&block)
218
- @actions << block if block
267
+ # Use this from Project#on_define to make each project definition yield
268
+ # to this block after it's done with the project block.
269
+ #
270
+ # Project.on_define do |project|
271
+ # puts "defining"
272
+ # project.after_block { puts "defined" }
273
+ # end
274
+ # define "foo" do
275
+ # puts "block"
276
+ # end
277
+ # => defining
278
+ # block
279
+ # defined
280
+ def after_block(&block)
281
+ @after_block << block if block
219
282
  end
220
283
 
221
- protected
222
-
284
+ # :nodoc:
223
285
  def _define(properties, &block)
224
286
  fail "Project #{name} already defined" if @defined
225
287
  @defined = true
226
- @base_dir = properties.delete(:base_dir) if properties.has_key?(:base_dir)
227
- # How convenient: project name is used to create compound namespace for tasks.
288
+ @base_dir = File.expand_path(properties.delete(:base_dir)) if properties.has_key?(:base_dir)
289
+ # How convenient: project name is used to create compound namespace for task.
290
+ # Keep in mind that we're already in a namespace context if it's a sub-project.
228
291
  namespace name.split(":").last do
229
- # On_create requires so we have attribute accessors for the properties.
230
- (self.class.instance_variable_get(:@on_create) || []).each { |callback| callback.call self }
231
- properties.each { |name, value| send "#{name}=", value }
232
- if block
233
- begin
234
- # Evaluate in context of project, and pass project. And for that we need
235
- # a method definition, on the singleton so we don't conflict with anyone else.
236
- singleton = (class << self ; self ; end)
237
- singleton.send :define_method, :__enhance__, &block
238
- self.__enhance__ self
239
- ensure
240
- singleton.send :remove_method, :__enhance__
292
+ # Everything we do inside the project is relative to its working directory.
293
+ Dir.chdir File.exist?(@base_dir) ? @base_dir : Dir.pwd do
294
+ # on_define blocks may have use for project properties.
295
+ properties.each { |name, value| send "#{name}=", value }
296
+ (self.class.instance_variable_get(:@on_define) || []).each { |callback| callback.call self }
297
+ if block
298
+ begin
299
+ # Evaluate in context of project, and pass project. And for that we need
300
+ # a method definition, on the singleton so we don't conflict with anyone else.
301
+ singleton = (class << self ; self ; end)
302
+ singleton.send :define_method, :__enhance__, &block
303
+ self.__enhance__ self
304
+ ensure
305
+ singleton.send :remove_method, :__enhance__
306
+ end
241
307
  end
308
+ # And now for all the callbacks created by on_define.
309
+ @after_block.each { |callback| callback.call self }
242
310
  end
243
- @actions.each { |callback| callback.call self }
244
- @actions.clear
245
311
  end
246
312
  self
247
313
  end
@@ -256,45 +322,93 @@ module Buildr
256
322
  # Defines a new project.
257
323
  #
258
324
  # The first argument is the project name. Each project must have a unique name,
259
- # to distinguish it from other projects. The name must be unique either across
260
- # all top-level projects, or all sub-projects belonging to the same parent.
325
+ # and you can only define a project once.
261
326
  #
262
327
  # The second argument contains any number of properties that are set on the
263
- # project at creation, before calling the block. There is no special preference
264
- # to properties passed as arguments.
328
+ # project. The project must have attribute accessors to support these properties.
329
+ # You can also pass the project name in the properties hash.
330
+ #
331
+ # The easiest way to define a project and configure its tasks is by passing
332
+ # a block. The #define method executes the block within the context of the
333
+ # project, as if with instance_eval. It also passes the project to the block.
265
334
  #
266
- # The second argument is optional. You may omit the first argument, by passing
267
- # the project name as the property :name.
335
+ # For example:
336
+ # define "foo", :version=>"1.0" do
337
+ # . . .
338
+ # end
268
339
  #
269
- # If a block is given, it is executed in the context of the project, and passed
270
- # a reference to the project. These two are equivalent:
271
- # define "foo", :version=>"1" do
272
- # self.group = "foo-s"
340
+ # define "bar" do |project|
341
+ # project.version = "1.0"
342
+ # . . .
273
343
  # end
274
344
  #
275
- # define "foo" do |project|
276
- # project.version = "1"
277
- # project.group = "foo-s"
345
+ # define "baz" do
346
+ # self.version = "1.0"
278
347
  # end
348
+ #
349
+ # Each project also has a #define method that operates the same way, but
350
+ # defines a sub-project. A sub-project has a compound name using the parent
351
+ # project's name, and also inherits some of its properties. You can only
352
+ # define a sub-project as part of the parent project's definition.
353
+ #
354
+ # For example:
355
+ # define "foo", :version=>"1.0" do
356
+ # define "bar"
357
+ # puts name
358
+ # puts version
359
+ # end
360
+ # end
361
+ # => "foo:bar"
362
+ # "1.0"
279
363
  def define(*args, &block)
280
364
  Project.define(*args, &block)
281
365
  end
282
366
 
283
- # Returns a top-level project.
367
+ # Returns the named project.
368
+ #
369
+ # For a sub-project, use the full name, for example "foo:bar" to find
370
+ # the sub-project "bar" of the parent project "foo".
371
+ #
372
+ # You can reference a project before its definition. Be careful, though,
373
+ # since the referenced project will not have all its properties and tasks
374
+ # properly set. However, some tasks handle this well by accepting the
375
+ # project object and accessing it only when invoked, after all projects
376
+ # are defined.
377
+ #
378
+ # For example:
379
+ # def "project1" do
380
+ # compile.with project("project2")
381
+ # end
382
+ #
383
+ # def "project2" do
384
+ # . . .
385
+ # end
284
386
  def project(name)
285
387
  Project.project(name)
286
388
  end
287
389
 
288
- task "check" do |task|
289
- # Find all projects that:
290
- # - Are referenced but never defined.
291
- # - Do not have a base directory.
292
- Project.projects.each do |project|
293
- warn "Project #{project.name} referenced but not defined" unless project.defined?
294
- warn "Project #{project.name} does not have a base directory" unless File.exist?(project.base_dir)
295
- end
390
+ # Returns a list of all the projects defined so far.
391
+ #
392
+ # For example:
393
+ # files = projects.map { |prj| FileList[prj.path_to("src/**/*.java") }.flatten
394
+ # puts "There are #{files.size} source files in #{projects.size} projects"
395
+ def projects()
396
+ Project.projects
296
397
  end
297
398
 
399
+
400
+ # The local directory task executes the same task on the project
401
+ # in the local directory.
402
+ #
403
+ # For example, if the current directory project is +foo+, then
404
+ # +rake task+ executes +rake foo:task+.
405
+ #
406
+ # The current directory project is a project with the base directory
407
+ # being the same as the current directory. For example:
408
+ # cd bar
409
+ # rake build
410
+ # Will execute the +foo:bar:build+ task, after switching to the directory
411
+ # of the sub-project +bar+.
298
412
  class LocalDirectoryTask < Rake::Task
299
413
 
300
414
  def initialize(*args)
@@ -310,4 +424,31 @@ module Buildr
310
424
 
311
425
  end
312
426
 
427
+
428
+ class CheckTask < Rake::Task
429
+
430
+ def execute()
431
+ @warnings = []
432
+ super
433
+ report
434
+ end
435
+
436
+ def note(*msg)
437
+ @warnings += msg
438
+ end
439
+
440
+ def report()
441
+ if @warnings.empty?
442
+ puts HighLine.new.color("No warnings", :green)
443
+ else
444
+ warn "These are possible problems with your Rakefile"
445
+ @warnings.each { |msg| warn " #{msg}" }
446
+ end
447
+ end
448
+
449
+ end
450
+
451
+ desc "Check your Rakefile for common errors"
452
+ CheckTask.define_task("check") { |task| task.note *Project.warnings }
453
+
313
454
  end