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. # # 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 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 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 "module1" do # package :jar # end # # define "module2" do # compile.with project("project1:module1") # 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" class Project < Rake::Task class << self # See Buildr#define. def define(*args, &block) name, properties = name_and_properties_from_args(*args) # 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" @projects ||= {} 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 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(*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. # # 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 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 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| 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) task_name end end include Attributes # 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 # :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. 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 @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.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. # # 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") # => /tmp def path_to(*args) File.expand_path(File.join(args.map { |arg| Symbol === arg ? send(arg) : arg.to_s }), base_dir) end # Same as Buildr#project. def project(name) Project.project(name) end # Same as Buildr#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. # # 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. 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.application.lookup(task_name, parent.name.split(":")).enhance [task] #Rake::Task["^#{name}"].enhance([ task ]) end task.enhance &block end end def execute() 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| ... } # # 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 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 in the context of 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 # end # end # => "foo:bar" # "1.0" def define(*args, &block) Project.define(*args, &block) end # 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 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: # define "project1" do # self.version = "1.1" # # 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 # 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" # # projects("project1", "project2").map(&:base_dir) def projects(*args) Project.projects *args end # Add project definition tests. task("check") { |task| task.note *Project.warnings } end