module Buildr # A project is a means to assemble multiple related tasks. # # 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. # # 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. # # For example: # define "parent" do |project| # project.version = "1.1" # # define "child1" # define "child2" # end # # This definition will work for a directory structure of: # . -- Parent project # |__child 1 # |__child 2 # # 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. # # 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. # # 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. 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 end # Returns the specified project (top-level only). def project(name) @projects ||= {} name.split(":").inject(nil) do |parent, name| if parent @projects["#{parent.name}:#{name}"] ||= Project.new(name, parent) else @projects[name] ||= Project.new(name, nil) end end end # Returns all project definitions, including sub-projects. def projects() (@projects || []).map { |name, project| project } 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. # # 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. # # 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 end # :nodoc: def name_and_properties_from_args(*args) if Hash === args.last properties = args.pop.clone 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." unless name [ name, properties ] end end include Attributes # The project name. attr_reader :name # The parent project if this is a sub-project. attr_reader :parent # The base directory of this project. attr_reader :base_dir # Always construct a project using Object#project or Project#project. 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 end @actions = [] 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 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. # # 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. # # For example: # path_to("foo", "bar") # path_to(:target_dir, "foo") # path_to("/tmp") # are equivalent to: # File.join(base_dir, "foo", "bar") # File.join(base_dir, project.target_dir, "foo") # "/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 def project(name) Project.project(name) end # Returns all sub-projects defined in this project, including their # sub-projects. def projects() prefix = name + ":" Project.projects.map { |project| project.starts_with?(prefix) } 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(":", "-") 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. # # For example: # 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. # # 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. 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 ]) end task.enhance &block end end def enhance(&block) @actions << block if block end protected 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. 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__ end end @actions.each { |callback| callback.call self } @actions.clear end self 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, # 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. # # 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. # # The second argument is optional. You may omit the first argument, by passing # the project name as the property :name. # # 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" # end # # define "foo" do |project| # project.version = "1" # project.group = "foo-s" # end def define(*args, &block) Project.define(*args, &block) end # Returns a top-level project. 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 end 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 end end