lib/core/project.rb in buildr-0.15.0 vs lib/core/project.rb in buildr-0.16.0

- old
+ new

@@ -1,55 +1,82 @@ module Buildr - # A project is a means to assemble multiple related tasks. + # 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 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. + # 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 can be organized hierarchically such that sub-projects - # inherit default properties from their parent project, and - # participate in tasks executed on the parent project. + # Projects have properties, some of which they inherit from their + # parent project. Built it 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. + # # For example: - # define "parent" do |project| - # project.version = "1.1" + # define "project1" do + # self.version = "1.1" # - # define "child1" - # define "child2" + # define "module1" do + # package :jar + # end + # + # define "module2" do + # compile.with project("project1:module1") + # package :jar + # end # end # - # This definition will work for a directory structure of: - # . -- Parent project - # |__child 1 - # |__child 2 + # project("project1").projects.map(&:name) + # => [ "project1:module1", "project1:module2" ] + # project("project1:module1").parent.name + # => "project1" + # project("project1:module1").version + # => "1.1" # - # 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. + # 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. # - # 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. + # For the above example, the directory structure is: + # . + # |__module1 + # |__module2 # - # 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. + # 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| + # project.version = "1.1" + # self.version = "1.1" + # end 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 + 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 - # Returns the specified project (top-level only). + # See Buildr#project. def project(name) @projects ||= {} name.split(":").inject(nil) do |parent, name| if parent @projects["#{parent.name}:#{name}"] ||= Project.new(name, parent) @@ -57,32 +84,38 @@ @projects[name] ||= Project.new(name, nil) end end end - # Returns all project definitions, including sub-projects. + # See Buildr#projects. def projects() - (@projects || []).map { |name, project| project } + (@projects ||= {}).map { |name, project| project }.select(&:defined?).sort_by(&:name) 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. + # 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. # - # 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. + # 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 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 + # 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. + def on_define(&block) + (@on_define ||= []) << block if block end # :nodoc: def name_and_properties_from_args(*args) if Hash === args.last @@ -94,27 +127,49 @@ 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 + 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} 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 + end include Attributes - # The project name. + # 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 - # The base directory of this project. + # 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 - # Always construct a project using Object#project or Project#project. + # :nodoc: def initialize(name, parent) fail "Missing project name" unless name @name = parent ? "#{parent.name}:#{name}" : name @parent = parent if parent @@ -123,127 +178,138 @@ @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 = [] + @after_block = [] + @defined = false 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) + project("#{self.name}:#{name}")._define properties, &block end + # Returns true if the project was already defined. 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. + 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. # - # 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. + # 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") - # are equivalent to: - # File.join(base_dir, "foo", "bar") - # File.join(base_dir, project.target_dir, "foo") - # "/tmp" + # => /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 + # Same as Buildr#project. def project(name) Project.project(name) end - # Returns all sub-projects defined in this project, including their - # sub-projects. + # Same as Buildr#projects. def projects() - prefix = name + ":" - Project.projects.map { |project| project.starts_with?(prefix) } + Project.projects 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(":", "-") + def sub_projects() + prefix = name + ":" + Project.projects.select { |project| project.name.starts_with?(prefix) }.sort_by(&:name) 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. + # 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. + # Will execute foo:bar:build and foo:baz: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. + # 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| if parent - Rake::Task["^#{parent.name}:#{name}"].enhance([ task ]) + Rake::Task["^#{name}"].enhance([ task ]) end task.enhance &block end end - def enhance(&block) - @actions << block if block + # 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 - protected - + # :nodoc: 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. + @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 - # 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__ + # 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 - @actions.each { |callback| callback.call self } - @actions.clear end self end end @@ -254,49 +320,97 @@ # 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. + # and you can only define a project once. # # 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. + # 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. You may omit the first argument, by passing - # the project name as the property :name. + # 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. # - # 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" + # For example: + # define "foo", :version=>"1.0" do + # . . . # end # - # define "foo" do |project| - # project.version = "1" - # project.group = "foo-s" + # define "bar" do |project| + # project.version = "1.0" + # . . . # end + # + # 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. + # + # 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 a top-level project. + # 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. + # + # For example: + # def "project1" do + # compile.with project("project2") + # end + # + # def "project2" do + # . . . + # end 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 + # Returns a list of all the projects defined so far. + # + # 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| @@ -307,7 +421,34 @@ projects.each { |project| task("#{project.name}:#{task.name}").invoke } end end 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 } end