lib/core/project.rb in buildr-0.16.0 vs lib/core/project.rb in buildr-0.18.0
- old
+ new
@@ -8,23 +8,22 @@
# 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 have properties, some of which they inherit from their
- # parent project. Built it tasks use these properties, for example,
+ # parent project. Built in 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.
+ # You can only define a project once using #define. Afterwards, you
+ # can obtain the project definition using #project. However, when
+ # working with sub-projects, one project may reference another ahead
+ # of its definition: the sub-project definitions are then evaluated
+ # based on their dependencies with each other. Circular dependencies
+ # are not allowed.
#
# For example:
# define "project1" do
# self.version = "1.1"
#
@@ -36,11 +35,13 @@
# compile.with project("project1:module1")
# package :jar
# end
# end
#
- # project("project1").projects.map(&:name)
+ # projects.map(&:name)
+ # => [ "project", "project:module1", "project1:module2" ]
+ # project("project1").sub_projects.map(&:name)
# => [ "project1:module1", "project1:module2" ]
# project("project1:module1").parent.name
# => "project1"
# project("project1:module1").version
# => "1.1"
@@ -48,56 +49,101 @@
# 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.
#
# For the above example, the directory structure is:
- # .
- # |__module1
- # |__module2
+ # project1/
+ # |__Rakefile
+ # |__module1/
+ # |__module2/
#
- # 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|
+ # The project definition tasks a block and executes it in the context of
+ # the project object. For example:
+ # define "project1" do
# project.version = "1.1"
- # self.version = "1.1"
# end
- class Project
+ # puts project("project1").version
+ # => "1.1"
+ class Project < Rake::Task
class << self
# See Buildr#define.
def define(*args, &block)
name, properties = name_and_properties_from_args(*args)
- 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
+ # Make sure a sub-project is only defined within the parent project,
+ # to prevent silly mistakes that lead to inconsistencies (e.g.
+ # namespaces will be all out of whack).
+ Rake.application.current_scope == name.split(":")[0...-1] or
+ raise "You can only define a sub project (#{name}) within the definition of its parent process"
- # See Buildr#project.
- def project(name)
@projects ||= {}
- name.split(":").inject(nil) do |parent, name|
- if parent
- @projects["#{parent.name}:#{name}"] ||= Project.new(name, parent)
+ raise "You cannot define the same project (#{name}) more than once" if @projects[name]
+ Project.define_task(name).tap do |project|
+ @projects[name] = project
+ project.enhance { |project| @on_define.each { |callback| callback[project] } } if @on_define
+ # Set the project properties first, actions may use them.
+ properties.each { |name, value| project.send "#{name}=", value }
+ # Enhance the project definition with the block.
+ if block
+ project.enhance { project.instance_eval &block }
+ end
+
+ if project.parent
+ project.parent.enhance { project.invoke }
else
- @projects[name] ||= Project.new(name, nil)
+ project.invoke
end
end
end
+ # See Buildr#project.
+ def project(name)
+ @projects && @projects[name] or raise "No such project #{name}"
+ returning(@projects[name]) { |project| project.invoke }
+ end
+
# See Buildr#projects.
- def projects()
- (@projects ||= {}).map { |name, project| project }.select(&:defined?).sort_by(&:name)
+ def projects(*args)
+ @projects ||= {}
+ if args.empty?
+ @projects.keys.map { |name| project(name) }.sort_by(&:name)
+ else
+ args.map { |name| project(name) or raise "No such project #{name}" }.
+ uniq.sort_by(&:name)
+ end
end
# Discard all project definitions.
def clear()
@projects.clear if @projects
end
+ # Enhances this task into a local task. A local task executes the same
+ # task on the project in the local directory.
+ #
+ # For example, if the current directory project is +foo+, then
+ # +rake build+ executes +rake foo:build+.
+ #
+ # 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+.
+ def local_task(task)
+ task = task(task) unless Rake::Task === task
+ task.enhance do |task|
+ projects = Project.projects.select { |project| project.base_dir == Rake.application.original_dir }
+ if verbose && projects.empty?
+ warn "No projects defined for directory #{Rake.application.original_dir}"
+ end
+ projects.each { |project| task("#{project.name}:#{task.name}").invoke }
+ end
+ task
+ end
+
# 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.
#
@@ -108,20 +154,32 @@
# end
#
# 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.
+ # want to use the #enhance method instead, by calling it
+ # from within #on_define.
+ #
+ # For example:
+ # Project.on_define do |project|
+ # puts "defining"
+ # project.enhance { puts "defined" }
+ # end
+ # define "foo" do
+ # puts "block"
+ # end
+ # => defining
+ # block
+ # defined
def on_define(&block)
(@on_define ||= []) << block if block
end
# :nodoc:
def name_and_properties_from_args(*args)
if Hash === args.last
- properties = args.pop.clone
+ properties = args.pop.dup
else
properties = {}
end
if String === args.first
name = args.shift
@@ -139,16 +197,19 @@
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
+ def scope_name(scope, task_name)
+ task_name
+ end
+
end
include Attributes
# The project name. If this is a sub-project, it will be prefixed
@@ -156,51 +217,62 @@
attr_reader :name
# The parent project if this is a sub-project.
attr_reader :parent
+ # :nodoc:
+ def initialize(*args)
+ super
+ split = name.split(":")
+ if split.size > 1
+ # Get parent project, but do not invoke it's definition to
+ # prevent circular dependencies (it's being invoked right now).
+ @parent = task(split[0...-1].join(":"))
+ raise "No parent project #{split[0...-1].join(":")}" unless @parent && Project === parent
+ end
+ # We want to lazily evaluate base_dir, but default initialize
+ # will set it to the current directory.
+ @base_dir = nil
+ end
+
# 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
-
- # :nodoc:
- def initialize(name, parent)
- fail "Missing project name" unless name
- @name = parent ? "#{parent.name}:#{name}" : name
- @parent = parent
- if parent
- # For sub-project, a good default is a directory in the parent's base_dir,
- # using the same name as the project.
- @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
+ def base_dir()
+ unless @base_dir
+ if @parent
+ # For sub-project, a good default is a directory in the parent's base_dir,
+ # using the same name as the project.
+ sub_dir = File.join(@parent.base_dir, name.split(":").last)
+ @base_dir = File.exist?(sub_dir) ? sub_dir : @parent.base_dir
+ @base_dir = sub_dir
+ else
+ # For top-level project, a good default is the directory where we found the Rakefile.
+ @base_dir = Dir.pwd
+ end
end
- @after_block = []
- @defined = false
+ @base_dir
end
+ # Set the base directory. Note: you can only do this once for a project,
+ # and only before accessing the base directory. If you try reading the
+ # value with #base_dir, the base directory cannot be set again.
+ def base_dir=(dir)
+ raise "Cannot set base directory twice, or after reading its value" if @base_dir
+ @base_dir = File.expand_path(dir)
+ end
+
# Define a new sub-project within this project.
def define(*args, &block)
name, properties = Project.name_and_properties_from_args(*args)
- project("#{self.name}:#{name}")._define properties, &block
+ Project.define "#{self.name}:#{name}", properties, &block
end
- # Returns true if the project was already defined.
- def defined?()
- @defined
- end
-
- 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.
#
# Symbol arguments are converted to paths by calling the attribute accessor
# on the project. For example:
@@ -220,19 +292,57 @@
def project(name)
Project.project(name)
end
# Same as Buildr#projects.
- def projects()
- Project.projects
+ def projects(*args)
+ Project.projects(*args)
end
def sub_projects()
prefix = name + ":"
Project.projects.select { |project| project.name.starts_with?(prefix) }.sort_by(&:name)
end
+ # Create or return a file task. This is similar to Rake's file method,
+ # with the exception that all relative paths are resolved relative to
+ # the project's base directory.
+ #
+ # You can call this from within or outside the project definition.
+ def file(args, &block)
+ task_name, deps = Rake.application.resolve_args(args)
+ unless task = Rake.application.lookup(task_name, [])
+ task = Rake::FileTask.define_task(File.expand_path(task_name, base_dir))
+ task.base_dir = base_dir
+ end
+ deps = [deps] unless deps.respond_to?(:to_ary)
+ task.enhance deps, &block
+ end
+
+ # Create or return a task. This is similar to Rake's task method,
+ # with the exception that the task is always defined within the project's
+ # namespace.
+ #
+ # If called from within the project definition, it returns a task,
+ # creating a new one no such task exists. If called from outside the
+ # project definition, it returns a task and raises an error if the
+ # task does not exist.
+ def task(args, &block)
+ task_name, deps = Rake.application.resolve_args(args)
+ if Rake.application.current_scope == name.split(":")
+ Rake::Task.define_task(task_name=>deps, &block)
+ else
+ if task = Rake.application.lookup(task_name, name.split(":"))
+ deps = [deps] unless deps.respond_to?(:to_ary)
+ task.enhance deps, &block
+ else
+ full_name = "#{name}:#{task_name}"
+ raise "You cannot define a project task outside the project definition, and no task #{full_name} defined in the project"
+ end
+ end
+ end
+
# Define a recursive task.
#
# 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.
@@ -252,66 +362,27 @@
# rake build
# Will execute foo:bar:build and foo:baz:build.
#
# 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|
+ def recursive_task(args, &block)
+ task_name, deps = Rake.application.resolve_args(args)
+ deps = [deps] unless deps.respond_to?(:to_ary)
+ returning(task(task_name=>deps)) do |task|
if parent
- Rake::Task["^#{name}"].enhance([ task ])
+ Rake.application.lookup(task_name, parent.name.split(":")).enhance [task]
+ #Rake::Task["^#{name}"].enhance([ task ])
end
task.enhance &block
end
end
- # 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
-
- # :nodoc:
- def _define(properties, &block)
- fail "Project #{name} already defined" if @defined
- @defined = true
- @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
+ def execute()
+ Rake.application.in_namespace ":#{name}" do
# 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
+ Dir.chdir(base_dir) { super }
end
- self
end
end
# :call-seq:
@@ -327,27 +398,23 @@
# The second argument contains any number of properties that are set on the
# project. The project must have attribute accessors to support these properties.
# You can also pass the project name in the properties hash.
#
# 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.
+ # a block. The #define method executes the block in the context of the project.
#
# For example:
# define "foo", :version=>"1.0" do
- # . . .
+ # puts version
# end
#
- # define "bar" do |project|
- # project.version = "1.0"
- # . . .
+ # define "bar" do
+ # puts name
# end
+3 # => "1.0"
+ # "bar"
#
- # 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.
#
@@ -367,88 +434,47 @@
# 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.
+ # You cannot reference a project before the project is defined, with one
+ # exception. When working with sub-projects, the project definitions are
+ # stored but not executed until the parent project is defined. So within
+ # a sub-project definition you can reference another sub-project definition.
+ # The definitions are then performed (invoked) based on that dependency.
+ # You cannot have circular references between project definitions.
#
# For example:
- # def "project1" do
- # compile.with project("project2")
- # end
+ # define "project1" do
+ # self.version = "1.1"
#
- # def "project2" do
- # . . .
+ # define "module1" do
+ # package :jar
+ # end
+ #
+ # define "module2" do
+ # compile.with project("project1:module1")
+ # package :jar
+ # end
# end
def project(name)
Project.project(name)
end
- # Returns a list of all the projects defined so far.
+ # With no arguments, returns a list of all the projects defined so far.
#
+ # With arguments, returns a list of these projects. Equivalent to calling #project
+ # for each named project.
+ #
# 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|
- projects = Project.projects.select { |project| project.base_dir == Rake.application.original_dir }
- if verbose && projects.empty?
- warn "No projects defined for directory #{Rake.application.original_dir}"
- end
- projects.each { |project| task("#{project.name}:#{task.name}").invoke }
- end
- end
-
+ # projects("project1", "project2").map(&:base_dir)
+ def projects(*args)
+ Project.projects *args
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 }
+ # Add project definition tests.
+ task("check") { |task| task.note *Project.warnings }
end