lib/core/project.rb in buildr-0.18.0 vs lib/core/project.rb in buildr-0.19.0

- old
+ new

@@ -1,250 +1,294 @@ +require "core/rake_ext" + module Buildr - # 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. + # An inherited attribute gets its value an accessor with the same name. + # But if the value is not set, it will obtain a value from the parent, + # so setting the value in the parent make it accessible to all the children + # that did not override it. + module InheritedAttributes + + class << self + private + def included(mod) + mod.extend(self) + end + end + + # :call-seq: + # inherited_attr(symbol, default?) + # inherited_attr(symbol) { |obj| ... } + # + # Defines an inherited attribute. The first form can provide a default value + # for the top-level object, used if the attribute was not set. The second form + # provides a default value by calling the block. + # + # For example: + # inherited_attr :version + # inherited_attr :src_dir, "src" + # inherited_attr(:created_on) { Time.now } + def inherited_attr(symbol, default = nil, &block) + block ||= proc { default } + attr_accessor symbol + define_method "#{symbol}_with_inheritence" do + unless value = send("#{symbol}_without_inheritence") + value = parent ? parent.send(symbol) : self.instance_eval(&block) + send "#{symbol}=", value + end + value + end + alias_method_chain symbol, :inheritence + end + end + + + # A project definition is where you define all the tasks associated with + # the project you're building. # - # 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+. + # The project itself will define several life cycle tasks for you. For example, + # it automatically creates a compile task that will compile all the source files + # found in src/main/java into target/classes, a test task that will compile source + # files from src/test/java and run all the JUnit tests found there, and a build + # task to compile and then run the tests. # - # Projects have properties, some of which they inherit from their - # 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 use the project definition to enhance these tasks, for example, telling the + # compile task which class path dependencies to use. Or telling the project how + # to package an artifact, e.g. creating a JAR using <tt>package :jar</tt>. # + # You can also define additional tasks that are executed by project tasks, + # or invoked from rake. + # + # Tasks created by the project are all prefixed with the project name, e.g. + # the project foo creates the task foo:compile. If foo contains a sub-project bar, + # the later will define the task foo:bar:compile. Since the compile task is + # recursive, compiling foo will also compile foo:bar. + # + # If you run: + # rake compile + # from the command line, it will execute the compile task of the current project. + # + # Projects and sub-projects follow a directory heirarchy. The Rakefile is assumed to + # reside in the same directory as the top-level project, and each sub-project is + # contained in a sub-directory in the same name. For example: + # /home/foo + # |__ Rakefile + # |__ src/main/java + # |__ foo + # |__ src/main/java + # + # The default structure of each project is assumed to be: + # src + # |__main + # | |__java <-- Source files to compile + # | |__resources <-- Resources to copy + # | |__webapp <-- For WARs + # |__test + # | |__java <-- Source files to compile (tests) + # | |__resources <-- Resources to copy (tests) + # |__target <-- Packages created here + # | |__classes <-- Generated when compiling + # | |__test-classes <-- Generated when compiling tests + # # 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" + # define "myapp", :version=>"1.1" do # - # define "module1" do - # package :jar + # define "wepapp" do + # compile.with project("myapp:beans") + # package :war # end # - # define "module2" do - # compile.with project("project1:module1") + # define "beans" do + # compile.with DEPENDS # package :jar # end # end # - # 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" - # - # 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: - # project1/ - # |__Rakefile - # |__module1/ - # |__module2/ - # - # 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" - # end - # puts project("project1").version - # => "1.1" + # puts projects.map(&:name) + # => [ "myapp", "myapp:beans", "myapp:webapp" ] + # puts project("myapp:webapp").parent.name + # => "myapp" + # puts project("myapp:webapp").compile.classpath.map(&:to_spec) + # => "myapp:myapp-beans:jar:1.1" class Project < Rake::Task class << self # See Buildr#define. - def define(*args, &block) - name, properties = name_and_properties_from_args(*args) + def define(name, properties, &block) #:nodoc: # 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" + raise "You can only define a sub project (#{name}) within the definition of its parent project" @projects ||= {} raise "You cannot define the same project (#{name}) more than once" if @projects[name] Project.define_task(name).tap do |project| + # Define the project to prevent duplicate definition. @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 - + properties.each { |name, value| project.send "#{name}=", value } if properties + project.enhance do |project| + @on_define.each { |callback| callback[project] } + end if @on_define + # Enhance the project using the definition block. + project.enhance { project.instance_eval &block } if block + + # Top-level project? Invoke the project definition. Sub-project? We don't invoke + # the project definiton yet (allow project() calls to establish order of evaluation), + # but must do so before the parent project's definition is done. if project.parent project.parent.enhance { project.invoke } else project.invoke end end end # See Buildr#project. - def project(name) + def project(name) #:nodoc: @projects && @projects[name] or raise "No such project #{name}" - returning(@projects[name]) { |project| project.invoke } + @projects[name].tap { |project| project.invoke } end # See Buildr#projects. - def projects(*args) + def projects(*names) #:nodoc: @projects ||= {} - if args.empty? + if names.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) + names.map { |name| project(name) or raise "No such project #{name}" }.uniq.sort_by(&:name) end end + # :call-seq: + # clear() + # # 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. + # :call-seq: + # local_task(name) + # local_task(name) { |name| ... } # - # For example, if the current directory project is +foo+, then - # +rake build+ executes +rake foo:build+. + # Defines a local task with an optional execution message. # - # The current directory project is a project with the base directory - # being the same as the current directory. For example: + # A local task is a task that executes a task with the same name, defined in the + # current project, the project's with a base directory that is the same as the + # current directory. + # + # Complicated? Try this: + # rake build + # is the same as: + # rake foo:build + # But: # 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| + # is the same as: + # rake foo:bar:build + # + # The optional block is called with the project name when the task executes + # and returns a message that, for example "Building project #{name}". + def local_task(args, &block) + task args 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}" + if projects.empty? + warn "No projects defined for directory #{Rake.application.original_dir}" if verbose + else + projects.each do |project| + puts block.call(project.name) if block && verbose + task("#{project.name}:#{task.name}").invoke + end 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. + # :call-seq: + # on_define() { |project| ... } # + # The Project class defines minimal behavior, only what is documented here. + # To extend its definition, other modules use Project#on_define to incorporate + # code called during a new project's definition. + # # For example: # # Set the default version of each project to "1.0". - # Project.on_define do |project| - # project.version ||= "1.0" - # end + # Project.on_define { |project| project.version ||= "1.0" } # - # 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 #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 + # Since each project definition is essentially a task, if you need to do work + # at the end of the project definition (after the block is executed), you can + # enhance it from within #on_define. 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.dup - else - properties = {} - end - if String === args.first - 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, this is the first argument to the define method" unless name - [ name, properties ] - end - - # :nodoc: - def warnings() - returning([]) do |msgs| + def warnings() #:nodoc: + [].tap 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} 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) + def scope_name(scope, task_name) #:nodoc: task_name end end - include Attributes + include InheritedAttributes - # 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". + # The project name. For example, "foo" for the top-level project, and "foo:bar" + # for its sub-project. attr_reader :name # The parent project if this is a sub-project. attr_reader :parent - # :nodoc: - def initialize(*args) + def initialize(*args) #:nodoc: 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). + # Get parent project, but do not invoke it's definition to prevent circular + # dependencies (it's being invoked right now, so calling project() will fail). @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. + # We only need this because each task (and a project is a task) already has + # a @base_dir variable (and base_dir method), and we want it lazily evaluated. + # See all the logic that happens when we call base_dir. @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. + # :call-seq: + # base_dir() => path # - # 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. + # Returns the project's base directory. + # + # The Rakefile defines top-level project, so it's logical that the top-level project's + # base directory is the one in which we find the Rakefile. And each sub-project has + # a base directory that is one level down, with the same name as the sub-project. + # + # For example: + # /home/foo/ <-- base_directory of project "foo" + # /home/foo/Rakefile <-- builds "foo" + # /home/foo/bar <-- sub-project "foo:bar" def base_dir() - unless @base_dir + if @base_dir.nil? 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 @@ -255,82 +299,133 @@ end end @base_dir end + # :call-seq: + # base_dir = dir + # + # Sets the project's base directory. Allows you to specify a base directory by calling + # this accessor, or with the :base_dir property when calling #define. + # + # You can only set the base directory once for a given project, and only before accessing + # the base directory (for example, by calling #file or #path_to). # 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.define "#{self.name}:#{name}", properties, &block - end - - # Returns a path made from multiple arguments. Relative paths are turned into - # absolute paths using this project's base directory. + # :call-seq: + # path_to(*names) => path # - # Symbol arguments are converted to paths by calling the attribute accessor - # on the project. For example: + # Returns a path from a combination of name, relative to the project's base directory. + # Essentially, joins all the supplied names and expands the path relative to #base_dir. + # 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 + # => /home/project1/foo/bar # path_to("/tmp") # => /tmp - def path_to(*args) - File.expand_path(File.join(args.map { |arg| Symbol === arg ? send(arg) : arg.to_s }), base_dir) + # path_to(:base_dir, "foo") + # => /home/project1/foo + def path_to(*names) + File.expand_path(File.join(names.map { |name| Symbol === name ? send(name) : name.to_s }), base_dir) end + # :call-seq: + # define(name, properties?) { |project| ... } => project + # + # Define a new sub-project within this project. See Buildr#define. + def define(name, properties = nil, &block) + Project.define "#{self.name}:#{name}", properties, &block + end + + # :call-seq: + # project(name) => project + # # Same as Buildr#project. def project(name) Project.project(name) end + # :call-seq: + # projects(*names) => projects + # # Same as Buildr#projects. - def projects(*args) - Project.projects(*args) + def projects(*names) + Project.projects(*names) 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. + # :call-seq: + # file(path) => Task + # file(path=>prereqs) => Task + # file(path) { |task| ... } => Task # - # You can call this from within or outside the project definition. + # Creates and returns a new file task in the project. Similar to calling Rake's + # file method, but the path is expanded relative to the project's base directory, + # and the task executes in the project's base directory. + # + # For example: + # define "foo" do + # define "bar" do + # file("src") { ... } + # end + # end + # + # puts project("foo:bar").file("src").to_s + # => "/home/foo/bar/src" 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. + # :call-seq: + # task(name) => Task + # task(name=>prereqs) => Task + # task(name) { |task| ... } => Task # - # 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. + # Creates and returns a new task in the project. Similar to calling Rake's task + # method, but prefixes the task name with the project name and executes the task + # in the project's base directory. + # + # For example: + # define "foo" do + # task "doda" + # end + # + # puts project("foo").task("doda").name + # => "foo:doda" + # + # When called from within the project definition, creates a new task if the task + # does not already exist. If called from outside the project definition, returns + # the named task and raises an exception if the task is not defined. + # + # As with Rake's task method, calling this method enhances the task with the + # prerequisites and optional block. def task(args, &block) task_name, deps = Rake.application.resolve_args(args) - if Rake.application.current_scope == name.split(":") + if task_name =~ /^:/ + Rake.application.instance_eval do + scope, @scope = @scope, [] + begin + Rake::Task.define_task(task_name[1..-1]=>deps, &block) + ensure + @scope = scope + end + end + elsif 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 @@ -339,140 +434,121 @@ 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. + # :call-seq: + # recursive_task(name=>prereqs) { |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. - # - # 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 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. + # Define a recursive task. A recursive task executes itself and the same task + # in all the sub-projects. 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| + task(task_name=>deps).tap do |task| if parent Rake.application.lookup(task_name, parent.name.split(":")).enhance [task] #Rake::Task["^#{name}"].enhance([ task ]) end task.enhance &block end end - def execute() + def execute() #:nodoc: Rake.application.in_namespace ":#{name}" do # Everything we do inside the project is relative to its working directory. Dir.chdir(base_dir) { super } end end end # :call-seq: - # define name { |project| ... } - # define name, properties { |project| ... } - # define properties { |project| ... } + # define(name, properties?) { |project| ... } => project # # Defines a new project. # - # The first argument is the project name. Each project must have a unique name, - # and you can only define a project once. + # The first argument is the project name. Each project must have a unique name. + # For a sub-project, the actual project name is created by prefixing the parent + # project's name. # - # 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 second argument is optional and contains a hash or properties that are set + # on the project. You can only use properties that are supported by the project + # definition, e.g. :group and :version. You can also set these properties from the + # project definition. # - # The easiest way to define a project and configure its tasks is by passing - # a block. The #define method executes the block in the context of the project. + # You pass a block that is executed in the context of the project definition. + # This block is used to define the project and tasks that are part of the project. + # Do not perform any work inside the project itself, as it will execute each time + # the Rakefile is loaded. Instead, use it to create and extend tasks that are + # related to the project. # # For example: # define "foo", :version=>"1.0" do - # puts version - # end # - # define "bar" do - # puts name - # end -3 # => "1.0" - # "bar" - # - # 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 + # define "bar" do + # compile.with "org.apache.axis2:axis2:jar:1.1" # end # end - # => "foo:bar" - # "1.0" - def define(*args, &block) - Project.define(*args, &block) + # + # puts project("foo").version + # => "1.0" + # puts project("foo:bar").compile.classpath.map(&:to_spec) + # => "org.apache.axis2:axis2:jar:1.1" + # % rake build + # => Compiling 14 source files in foo:bar + def define(name, properties = nil, &block) #:yields:project + Project.define(name, properties, &block) end - # Returns the named project. + # :call-seq: + # project(name) => project # - # For a sub-project, use the full name, for example "foo:bar" to find - # the sub-project "bar" of the parent project "foo". + # Returns a project definition. # - # 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. + # You cannot reference a project before the project is defined. When working with + # sub-projects, the project definition is stored by calling #define, and evaluated + # before a call to the parent project's #define method returns. # + # However, if you call #project with the name of another sub-project, its definition + # is evaluated immediately. So the returned project definition is always complete, + # and you can access its definition (e.g. to find files relative to the base directory, + # or packages created by that project). + # # For example: - # define "project1" do + # define "myapp" do # self.version = "1.1" # - # define "module1" do - # package :jar + # define "webapp" do + # # webapp is defined first, but beans is evaluated first + # compile.with project("myapp:beans") + # package :war # end # - # define "module2" do - # compile.with project("project1:module1") + # define "beans" do # package :jar # end # end def project(name) Project.project(name) end - # With no arguments, returns a list of all the projects defined so far. + # :call-seq: + # projects(*names) => projects # - # With arguments, returns a list of these projects. Equivalent to calling #project - # for each named project. + # With no arguments, returns a list of all projects defined so far. With arguments, + # returns a list of these projects, fails on undefined projects. # + # Like #project, this method evaluates the definition of each project before returning it. + # Be advised of circular dependencies. + # # For example: # files = projects.map { |prj| FileList[prj.path_to("src/**/*.java") }.flatten # puts "There are #{files.size} source files in #{projects.size} projects" # - # projects("project1", "project2").map(&:base_dir) - def projects(*args) - Project.projects *args + # puts projects("project1", "project2").map(&:base_dir) + def projects(*names) + Project.projects *names end # Add project definition tests. task("check") { |task| task.note *Project.warnings }