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