lib/core/project.rb in buildr-0.15.0 vs lib/core/project.rb in buildr-0.16.0
- old
+ new
@@ -1,55 +1,82 @@
module Buildr
- # A project is a means to assemble multiple related tasks.
+ # A project is a convenient mechanism for managing all the tasks
+ # related to a given project. For complex applications, you may have
+ # several projects, or sub-projects for each of the modules.
#
- # A project will create its own set of internal tasks, such as
- # compile, build, clean, install. These tasks are fed with project
- # properties, e.g. the clean task will remove the project.target_dir
- # directory, where files are created during the build process.
+ # A project definition creates its own set of tasks, prefixed with
+ # the project name. For example, each project has a clean, build
+ # and deploy task. For project +foo+ the task names are +foo:clean+,
+ # +foo:build+ and +foo:deploy+.
#
- # Projects can be organized hierarchically such that sub-projects
- # inherit default properties from their parent project, and
- # participate in tasks executed on the parent project.
+ # Projects have properties, some of which they inherit from their
+ # parent project. Built it tasks use these properties, for example,
+ # the +clean+ task will remove the target directory specified by
+ # the +target_dir+ property. The +compile+ tasks uses the compiler
+ # option: you can set these options on the parent project and they
+ # will be inherited by all sub-projects.
#
+ # You can only define a project once using #define. You can obtain
+ # the project using #project. Note that, if you're obtain the project
+ # object before the project is defined, the values of the project
+ # properties are not set yet, neither are any of the default tasks.
+ # However, many tasks perform late binding, and so you can pass a
+ # project before defining it. For example, the +compile+ task can
+ # take a project definition and use it as a classpath dependency.
+ #
# For example:
- # define "parent" do |project|
- # project.version = "1.1"
+ # define "project1" do
+ # self.version = "1.1"
#
- # define "child1"
- # define "child2"
+ # define "module1" do
+ # package :jar
+ # end
+ #
+ # define "module2" do
+ # compile.with project("project1:module1")
+ # package :jar
+ # end
# end
#
- # This definition will work for a directory structure of:
- # . -- Parent project
- # |__child 1
- # |__child 2
+ # project("project1").projects.map(&:name)
+ # => [ "project1:module1", "project1:module2" ]
+ # project("project1:module1").parent.name
+ # => "project1"
+ # project("project1:module1").version
+ # => "1.1"
#
- # The sub-projects child1 and child2 inherit the project version number
- # from the parent project. In addition, for certain tasks such as build,
- # running the task on the parent project will also run it on all
- # sub-projects.
+ # Each project has a base directory (see #base_dir). By default,
+ # a top-level project uses the current directory, and each sub-project
+ # uses a sub-directory relative to the parent project.
#
- # Use the #define method to define a new project, and within the context
- # of a project to define a sub-project. Use the #project method to find
- # an existing project, and within the context of a project, one of its
- # sub-projects.
+ # For the above example, the directory structure is:
+ # .
+ # |__module1
+ # |__module2
#
- # The main block in the project definition is executed during the project
- # definition. Use it to set project properties, and configure tasks.
- # Do not do any actual work in there.
+ # The project definition tasks a block and yields by passing the project
+ # definition. For convenience, the block is also executed in the context
+ # of the project object, as if with instance_eval.
+ #
+ # The following two are equivalent:
+ # define "project1" do |project|
+ # project.version = "1.1"
+ # self.version = "1.1"
+ # end
class Project
class << self
# See Buildr#define.
def define(*args, &block)
name, properties = name_and_properties_from_args(*args)
- project(name).send :_define, properties, &block
+ 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 =~ /:/
+ project(name)._define properties, &block
end
- # Returns the specified project (top-level only).
+ # See Buildr#project.
def project(name)
@projects ||= {}
name.split(":").inject(nil) do |parent, name|
if parent
@projects["#{parent.name}:#{name}"] ||= Project.new(name, parent)
@@ -57,32 +84,38 @@
@projects[name] ||= Project.new(name, nil)
end
end
end
- # Returns all project definitions, including sub-projects.
+ # See Buildr#projects.
def projects()
- (@projects || []).map { |name, project| project }
+ (@projects ||= {}).map { |name, project| project }.select(&:defined?).sort_by(&:name)
end
# Discard all project definitions.
def clear()
@projects.clear if @projects
end
- # The Project class defines very little behavior for new process instances.
- # Use #on_create to add behavior to new process instances.
+ # The Project class defines minimal behavior for new projects.
+ # Use #on_define to add behavior when defining new projects.
+ # Whenever a new project is defined, it will yield to the block
+ # with the project object.
#
- # Every block you register with #on_create will be called with a project
- # instance whenever a new project is created. You can then define tasks,
- # set project properties, etc.
+ # For example:
+ # # Set the default version of each project to "1.0".
+ # Project.on_define do |project|
+ # project.version ||= "1.0"
+ # end
#
- # Keep in mind that the order on which #on_create blocks are called is
- # not determined. You cannot depend on tasks defined by another block to
- # exist at the time your block is called. Use Project#enhance for that.
- def on_create(&block)
- (@on_create ||= []) << block if block
+ # Keep in mind that the order in which #on_define blocks are
+ # called is not determined. You cannot depend on a previous
+ # #on_define to set properties or create new tasks. You would
+ # want to use the #after_block method instead, by calling it
+ # from within #after_block.
+ def on_define(&block)
+ (@on_define ||= []) << block if block
end
# :nodoc:
def name_and_properties_from_args(*args)
if Hash === args.last
@@ -94,27 +127,49 @@
name = args.shift
else
name = properties.delete(:name)
end
raise ArgumentError, "Expected project name followed by (optional) project properties." unless args.empty?
- raise ArgumentError, "Missing project name." unless name
+ raise ArgumentError, "Missing project name, this is the first argument to the define method" unless name
[ name, properties ]
end
+
+ # :nodoc:
+ def warnings()
+ returning([]) do |msgs|
+ msgs << "There are no project definitions in your Rakefile" if @projects.nil? || @projects.empty?
+ # Find all projects that:
+ # * Are referenced but never defined. This is probably a typo.
+ # * Do not have a base directory.
+ (@projects || {}).each do |name, project|
+ msgs << "Project #{name} is referenced but not defined; you probably have a typo somewhere" unless project.defined?
+ msgs << "Project #{name} refers to the directory #{project.base_dir}, which does not exist" unless File.exist?(project.base_dir)
+ end
+ end
+ end
+
end
include Attributes
- # The project name.
+ # The project name. If this is a sub-project, it will be prefixed
+ # by the parent project's name. For example, "foo" and "foo:bar".
attr_reader :name
# The parent project if this is a sub-project.
attr_reader :parent
- # The base directory of this project.
+ # The base directory of this project. The default for a top-level project
+ # is the same directory that holds the Rakefile. The default for a
+ # sub-project is a child directory with the same name.
+ #
+ # A project definition can change the base directory using the base_dir
+ # hash value. Be advised that the base directory and all values that
+ # depend on it can only be determined after the project is defined.
attr_reader :base_dir
- # Always construct a project using Object#project or Project#project.
+ # :nodoc:
def initialize(name, parent)
fail "Missing project name" unless name
@name = parent ? "#{parent.name}:#{name}" : name
@parent = parent
if parent
@@ -123,127 +178,138 @@
@base_dir = File.join(parent.base_dir, name)
else
# For top-level project, a good default is the directory where we found the Rakefile.
@base_dir = Dir.pwd
end
- @actions = []
+ @after_block = []
+ @defined = false
end
# Define a new sub-project within this project.
def define(*args, &block)
name, properties = Project.name_and_properties_from_args(*args)
- Project.define("#{self.name}:#{name}", properties, &block)
+ project("#{self.name}:#{name}")._define properties, &block
end
+ # Returns true if the project was already defined.
def defined?()
@defined
end
- # Returns a path made from the specified arguments. Relative paths are turned
- # into absolute paths using the based directory of this project.
+ def to_s()
+ name
+ end
+
+ # Returns a path made from multiple arguments. Relative paths are turned into
+ # absolute paths using this project's base directory.
#
- # If you pass multiple arguments, they are combined into a path using File#join.
- # If one of the arguments is a symbol, it is used to retrieve that process
- # property.
+ # Symbol arguments are converted to paths by calling the attribute accessor
+ # on the project. For example:
#
# For example:
# path_to("foo", "bar")
+ # => /projects/project1/foo/bar
# path_to(:target_dir, "foo")
+ # => /projects/project1/target/foo
# path_to("/tmp")
- # are equivalent to:
- # File.join(base_dir, "foo", "bar")
- # File.join(base_dir, project.target_dir, "foo")
- # "/tmp"
+ # => /tmp
def path_to(*args)
File.expand_path(File.join(args.map { |arg| Symbol === arg ? send(arg) : arg.to_s }), base_dir)
end
- # Returns the specified sub-project of this project, or any of its descendant,
- # or a top-level project.
- #
- # For example:
- # bar.project("baz")
- # will find the first match from:
- # foo:bar:baz
- # foo:baz
- # baz
+ # Same as Buildr#project.
def project(name)
Project.project(name)
end
- # Returns all sub-projects defined in this project, including their
- # sub-projects.
+ # Same as Buildr#projects.
def projects()
- prefix = name + ":"
- Project.projects.map { |project| project.starts_with?(prefix) }
+ Project.projects
end
- # The project ID is the project name, and for a sub-project the
- # parent project ID followed by the project name, separated with a
- # hyphen. For example, "foo" and "foo-bar".
- def id()
- name.gsub(":", "-")
+ def sub_projects()
+ prefix = name + ":"
+ Project.projects.select { |project| project.name.starts_with?(prefix) }.sort_by(&:name)
end
# Define a recursive task.
#
- # A recursive task for a project will execute all sub-project tasks of
- # the same name before it executes itself. In addition, if a task with
- # the same name exists (not prefixed with a project), it will execute
- # same task but only for the current project -- as determined by the
- # current working directory.
+ # A recursive task executes the task with the same name in the project,
+ # and in all its sub-projects. In fact, a recursive task actually adds
+ # itself as a prerequisite on the parent task.
#
# For example:
+ # define "foo" do
+ # define "bar" do
+ # define "baz" do
+ # end
+ # end
+ # end
+ #
# rake foo:build
# Will execute foo:build, foo:bar:build and foo:baz:build
#
# Inside the bar directory:
# rake build
- # Will execute foo:bar:build.
+ # Will execute foo:bar:build and foo:baz:build.
#
- # Note, this method is used to define the task as recursive. It will
- # also define the task if the task does not already exist. However,
- # you can define the task before calling #recursive_task, e.g. to
- # create a file task, or any other special purpose task.
+ # This method defines a RakeTask. If you need a different type of task,
+ # define the task first and then call #recursive_task.
def recursive_task(arg, &block)
name = Hash === arg ? arg.keys.first : arg
returning(task(arg)) do |task|
if parent
- Rake::Task["^#{parent.name}:#{name}"].enhance([ task ])
+ Rake::Task["^#{name}"].enhance([ task ])
end
task.enhance &block
end
end
- def enhance(&block)
- @actions << block if block
+ # Use this from Project#on_define to make each project definition yield
+ # to this block after it's done with the project block.
+ #
+ # Project.on_define do |project|
+ # puts "defining"
+ # project.after_block { puts "defined" }
+ # end
+ # define "foo" do
+ # puts "block"
+ # end
+ # => defining
+ # block
+ # defined
+ def after_block(&block)
+ @after_block << block if block
end
- protected
-
+ # :nodoc:
def _define(properties, &block)
fail "Project #{name} already defined" if @defined
@defined = true
- @base_dir = properties.delete(:base_dir) if properties.has_key?(:base_dir)
- # How convenient: project name is used to create compound namespace for tasks.
+ @base_dir = File.expand_path(properties.delete(:base_dir)) if properties.has_key?(:base_dir)
+ # How convenient: project name is used to create compound namespace for task.
+ # Keep in mind that we're already in a namespace context if it's a sub-project.
namespace name.split(":").last do
- # On_create requires so we have attribute accessors for the properties.
- (self.class.instance_variable_get(:@on_create) || []).each { |callback| callback.call self }
- properties.each { |name, value| send "#{name}=", value }
- if block
- begin
- # Evaluate in context of project, and pass project. And for that we need
- # a method definition, on the singleton so we don't conflict with anyone else.
- singleton = (class << self ; self ; end)
- singleton.send :define_method, :__enhance__, &block
- self.__enhance__ self
- ensure
- singleton.send :remove_method, :__enhance__
+ # Everything we do inside the project is relative to its working directory.
+ Dir.chdir File.exist?(@base_dir) ? @base_dir : Dir.pwd do
+ # on_define blocks may have use for project properties.
+ properties.each { |name, value| send "#{name}=", value }
+ (self.class.instance_variable_get(:@on_define) || []).each { |callback| callback.call self }
+ if block
+ begin
+ # Evaluate in context of project, and pass project. And for that we need
+ # a method definition, on the singleton so we don't conflict with anyone else.
+ singleton = (class << self ; self ; end)
+ singleton.send :define_method, :__enhance__, &block
+ self.__enhance__ self
+ ensure
+ singleton.send :remove_method, :__enhance__
+ end
end
+ # And now for all the callbacks created by on_define.
+ @after_block.each { |callback| callback.call self }
end
- @actions.each { |callback| callback.call self }
- @actions.clear
end
self
end
end
@@ -254,49 +320,97 @@
# define properties { |project| ... }
#
# Defines a new project.
#
# The first argument is the project name. Each project must have a unique name,
- # to distinguish it from other projects. The name must be unique either across
- # all top-level projects, or all sub-projects belonging to the same parent.
+ # and you can only define a project once.
#
# The second argument contains any number of properties that are set on the
- # project at creation, before calling the block. There is no special preference
- # to properties passed as arguments.
+ # project. The project must have attribute accessors to support these properties.
+ # You can also pass the project name in the properties hash.
#
- # The second argument is optional. You may omit the first argument, by passing
- # the project name as the property :name.
+ # The easiest way to define a project and configure its tasks is by passing
+ # a block. The #define method executes the block within the context of the
+ # project, as if with instance_eval. It also passes the project to the block.
#
- # If a block is given, it is executed in the context of the project, and passed
- # a reference to the project. These two are equivalent:
- # define "foo", :version=>"1" do
- # self.group = "foo-s"
+ # For example:
+ # define "foo", :version=>"1.0" do
+ # . . .
# end
#
- # define "foo" do |project|
- # project.version = "1"
- # project.group = "foo-s"
+ # define "bar" do |project|
+ # project.version = "1.0"
+ # . . .
# end
+ #
+ # define "baz" do
+ # self.version = "1.0"
+ # end
+ #
+ # Each project also has a #define method that operates the same way, but
+ # defines a sub-project. A sub-project has a compound name using the parent
+ # project's name, and also inherits some of its properties. You can only
+ # define a sub-project as part of the parent project's definition.
+ #
+ # For example:
+ # define "foo", :version=>"1.0" do
+ # define "bar"
+ # puts name
+ # puts version
+ # end
+ # end
+ # => "foo:bar"
+ # "1.0"
def define(*args, &block)
Project.define(*args, &block)
end
- # Returns a top-level project.
+ # Returns the named project.
+ #
+ # For a sub-project, use the full name, for example "foo:bar" to find
+ # the sub-project "bar" of the parent project "foo".
+ #
+ # You can reference a project before its definition. Be careful, though,
+ # since the referenced project will not have all its properties and tasks
+ # properly set. However, some tasks handle this well by accepting the
+ # project object and accessing it only when invoked, after all projects
+ # are defined.
+ #
+ # For example:
+ # def "project1" do
+ # compile.with project("project2")
+ # end
+ #
+ # def "project2" do
+ # . . .
+ # end
def project(name)
Project.project(name)
end
- task "check" do |task|
- # Find all projects that:
- # - Are referenced but never defined.
- # - Do not have a base directory.
- Project.projects.each do |project|
- warn "Project #{project.name} referenced but not defined" unless project.defined?
- warn "Project #{project.name} does not have a base directory" unless File.exist?(project.base_dir)
- end
+ # Returns a list of all the projects defined so far.
+ #
+ # For example:
+ # files = projects.map { |prj| FileList[prj.path_to("src/**/*.java") }.flatten
+ # puts "There are #{files.size} source files in #{projects.size} projects"
+ def projects()
+ Project.projects
end
+
+ # The local directory task executes the same task on the project
+ # in the local directory.
+ #
+ # For example, if the current directory project is +foo+, then
+ # +rake task+ executes +rake foo:task+.
+ #
+ # The current directory project is a project with the base directory
+ # being the same as the current directory. For example:
+ # cd bar
+ # rake build
+ # Will execute the +foo:bar:build+ task, after switching to the directory
+ # of the sub-project +bar+.
class LocalDirectoryTask < Rake::Task
def initialize(*args)
super
enhance do |task|
@@ -307,7 +421,34 @@
projects.each { |project| task("#{project.name}:#{task.name}").invoke }
end
end
end
+
+
+ class CheckTask < Rake::Task
+
+ def execute()
+ @warnings = []
+ super
+ report
+ end
+
+ def note(*msg)
+ @warnings += msg
+ end
+
+ def report()
+ if @warnings.empty?
+ puts HighLine.new.color("No warnings", :green)
+ else
+ warn "These are possible problems with your Rakefile"
+ @warnings.each { |msg| warn " #{msg}" }
+ end
+ end
+
+ end
+
+ desc "Check your Rakefile for common errors"
+ CheckTask.define_task("check") { |task| task.note *Project.warnings }
end