require "core/rake_ext" require "core/common" module Buildr # 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 value = send("#{symbol}_without_inheritence") if value.nil? 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. # # 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. # # 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 package :jar. # # 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: # buildr compile # from the command line, it will execute the compile task of the current project. # # Projects and sub-projects follow a directory heirarchy. The Buildfile 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 # |__ Buildfile # |__ 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. The order in which you define projects is not important, # project definitions are evaluated when you ask for them. Circular dependencies will not # work. Rake tasks are only created after the project is evaluated, so if you need to access # a task (e.g. compile) use project("foo").compile instead of task("foo:compile"). # # For example: # define "myapp", :version=>"1.1" do # # define "wepapp" do # compile.with project("myapp:beans") # package :war # end # # define "beans" do # compile.with DEPENDS # package :jar # end # end # # 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 # :call-seq: # define(name, properties?) { |project| ... } => project # # See Buildr#define. 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 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 # Set the project properties first, actions may use them. 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. project.parent.enhance { project.invoke } if project.parent end end # :call-seq: # project(name) => project # # See Buildr#project. def project(*args) #:nodoc: options = args.pop if Hash === args.last rake_check_options options, :scope if options raise ArgumentError, "Only one project name at a time" unless args.size == 1 @projects ||= {} name = args.first if options && options[:scope] # We assume parent project is evaluated. project = options[:scope].split(":").inject([[]]) { |scopes, scope| scopes << (scopes.last + [scope]) }. map { |scope| @projects[(scope + [name]).join(":")] }. select { |project| project }.last end unless project # Parent project not evaluated. name.split(":").tap { |parts| @projects[parts.first].invoke if parts.size > 1 } project = @projects[name] end raise "No such project #{name}" unless project project.invoke project end # :call-seq: # projects(*names) => projects # # See Buildr#projects. def projects(*names) #:nodoc: options = names.pop if Hash === names.last rake_check_options options, :scope if options @projects ||= {} names = names.flatten if options && options[:scope] # We assume parent project is evaluated. if names.empty? parent = @projects[options[:scope].to_s] or raise "No such project #{options[:scope]}" @projects.values.select { |project| project.parent == parent }.each { |project| project.invoke }. map { |project| [project] + projects(:scope=>project) }.flatten.sort_by(&:name) else names.uniq.map { |name| project(name, :scope=>options[:scope]) } end elsif names.empty? # Parent project(s) not evaluated so we don't know all the projects yet. @projects.values.each(&:invoke) @projects.keys.map { |name| project(name) or raise "No such project #{name}" }.sort_by(&:name) else # Parent project(s) not evaluated, for the sub-projects we may need to find. names.map { |name| name.split(":") }.select { |name| name.size > 1 }.map(&:first).uniq.each { |name| project(name) } names.uniq.map { |name| project(name) or raise "No such project #{name}" }.sort_by(&:name) end end # :call-seq: # clear() # # Discard all project definitions. def clear() @projects.clear if @projects end # :call-seq: # local_task(name) # local_task(name) { |name| ... } # # Defines a local task with an optional execution message. # # 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: # buildr build # is the same as: # buildr foo:build # But: # cd bar # buildr build # is the same as: # buildr 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| local_projects do |project| puts block.call(project.name) if block && verbose task("#{project.name}:#{task.name}").invoke end end end # :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 { |project| project.version ||= "1.0" } # # 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 def scope_name(scope, task_name) #:nodoc: task_name end def local_projects(dir = nil, &block) #:nodoc: dir = File.expand_path(dir || Rake.application.original_dir) projects = Project.projects.select { |project| project.base_dir == dir } if projects.empty? && dir != Dir.pwd && File.dirname(dir) != dir local_projects(File.dirname(dir), &block) elsif block if projects.empty? warn "No projects defined for directory #{Rake.application.original_dir}" if verbose else projects.each { |project| block[project] } end else projects end end end include InheritedAttributes # 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 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, 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 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 # :call-seq: # base_dir() => path # # Returns the project's base directory. # # The Buildfile defines top-level project, so it's logical that the top-level project's # base directory is the one in which we find the Buildfile. 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/Buildfile <-- builds "foo" # /home/foo/bar <-- sub-project "foo:bar" def 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. @base_dir = File.join(@parent.base_dir, name.split(":").last) else # For top-level project, a good default is the directory where we found the Buildfile. @base_dir = Dir.pwd 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 # :call-seq: # path_to(*names) => path # # 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. # # Keep in mind that all tasks are defined and executed relative to the Buildfile directory, # so you want to use #path_to to get the actual path within the project as a matter of practice. # # For example: # path_to("foo", "bar") # => /home/project1/foo/bar # path_to("/tmp") # => /tmp # path_to(:base_dir, "foo") # same as path_to("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 alias :_ :path_to # :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 # project => self # # Same as Buildr#project. This method is called on a project, so a relative name is # sufficient to find a sub-project. # # When called on a project without a name, returns the project itself. You can use that when # setting project properties, for example: # define "foo" do # project.version = "1.0" # end def project(*args) if Hash === args.last options = args.pop else options = {} end if args.empty? self else Project.project *(args + [{ :scope=>self.name }.merge(options)]) end end # :call-seq: # projects(*names) => projects # # Same as Buildr#projects. This method is called on a project, so relative names are # sufficient to find sub-projects. def projects(*args) if Hash === args.last options = args.pop else options = {} end Project.projects *(args + [{ :scope=>self.name }.merge(options)]) end # :call-seq: # file(path) => Task # file(path=>prereqs) => Task # file(path) { |task| ... } => Task # # 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) deps = [deps] unless deps.respond_to?(:to_ary) Rake::FileTask.define_task(path_to(task_name)=>deps, &block) end # :call-seq: # task(name) => Task # task(name=>prereqs) => Task # task(name) { |task| ... } => Task # # 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 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 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 # :call-seq: # recursive_task(name=>prereqs) { |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) task = Buildr.options.parallel ? multitask(task_name) : task(task_name) parent.task(task_name).enhance [task] if parent task.enhance deps, &block end def execute() #:nodoc: # Reset the namespace, so all tasks are automatically defined in the project's namespace. Rake.application.in_namespace(":#{name}") { super } end def inspect() #:nodoc: %Q{project(#{name.inspect})} end end # :call-seq: # define(name, properties?) { |project| ... } => project # # Defines a new project. # # 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 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. # # 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 Buildfile is loaded. Instead, use it to create and extend tasks that are # related to the project. # # For example: # define "foo", :version=>"1.0" do # # define "bar" do # compile.with "org.apache.axis2:axis2:jar:1.1" # end # end # # puts project("foo").version # => "1.0" # puts project("foo:bar").compile.classpath.map(&:to_spec) # => "org.apache.axis2:axis2:jar:1.1" # % buildr build # => Compiling 14 source files in foo:bar def define(name, properties = nil, &block) #:yields:project Project.define(name, properties, &block) end # :call-seq: # project(name) => project # # Returns a project definition. # # When called from outside a project definition, must reference the project by its # full name, e.g. "foo:bar" to access the sub-project "bar" in "foo". When called # from inside a project, relative names are sufficient, e.g. project("foo").project("bar") # will find the sub-project "bar" in "foo". # # 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 "myapp" do # self.version = "1.1" # # define "webapp" do # # webapp is defined first, but beans is evaluated first # compile.with project("beans") # package :war # end # # define "beans" do # package :jar # end # end # # puts project("myapp:beans").version def project(*args) Project.project *args end # :call-seq: # projects(*names) => projects # # With no arguments, returns a list of all projects defined so far. When called on a project, # returns all its sub-projects (direct descendants). # # With arguments, returns a list of named projects, fails on any name that does not exist. # As with #project, you can use relative names when calling this method on a project. # # 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" # # puts projects("myapp:beans", "myapp:webapp").map(&:name) # Same as: # puts project("myapp").projects.map(&:name) def projects(*args) Project.projects *args end # Forces all the projects to be evaluated before executing any other task. # If we don't do that, we don't get to have tasks available when running Rake. task("buildr:projects") { projects } Rake.application.top_level_tasks.unshift "buildr:projects" end