module Buildr # This module gives you a way to access the individual properties of an # artifact: id, group, type and version. It can also return the artifact # specification. module ActsAsArtifact class << self def included(mod) mod.extend self end end # The artifact id. attr_reader :id # The group id. attr_reader :group # The file type. attr_reader :type # The version number. attr_reader :version # Optional classifier. attr_reader :classifier # Returns the artifact specification as a hash. def to_spec_hash() base = { :group=>group, :id=>id, :type=>type, :version=>version } classifier.blank? ? base : base.merge(:classifier=>classifier) end alias_method :to_hash, :to_spec_hash # Returns the artifact specification, in the structure: # ::: def to_spec() classifier.blank? ? "#{group}:#{id}:#{type}:#{version}" : "#{group}:#{id}:#{type}:#{classifier}:#{version}" end # Apply specification to this artifact. def apply_spec(spec) case spec when String @group, @id, @type, @version, *rest = spec.split(":") unless rest.empty? @classifier, @version = @version, rest.shift fail "Expecting project:id:type:version or project:id:type:classifier:version, found #{spec}" unless rest.empty? end when Hash [:group, :id, :type, :version, :classifier].each { |key| instance_variable_set("@#{key}", spec[key]) } @type ||= DEFAULT_FILE_TYPE else raise ArgumentError, "Expecting a string or a hash" end self end def pom() return self if type.to_s == "pom" artifact(:group=>group, :id=>id, :version=>version, :type=>"pom", :classifier=>classifier) end end # Use the artifact and artifacts method to create artifact tasks. class Artifact < Rake::FileCreationTask # The default file type for artifacts, if not specified. DEFAULT_FILE_TYPE = "jar" include ActsAsArtifact class << self # Lookup an artifact task based on its specification. def lookup(spec) @artifacts ||= {} @artifacts[hash_to_spec(spec)] end # Register a task as an artifact. Returns the task when calling # #artifacts. For example, a project will use this to register the # packages it creates. def register(task) @artifacts ||= {} if task.respond_to?(:to_hash) @artifacts[hash_to_spec(task.to_hash)] = task else fail "Can only call with an artifact task" end end # Turn a spec into a hash. This method accepts a string, hash or any object # that responds to the method to_spec. There are several reasons you'll want # to use this method: # * You can pass it anything that could possibly be a spec, and get a hash. # * It will check that your spec includes the group identifier, artifact # identifier and version number, and set the file type, if not specified. # * It will always return a new specs hash. # * Calling to_s on the hash will return a spec string. # # :nodoc: def spec_to_hash(spec) return spec_to_hash(spec.to_spec) if spec.respond_to?(:to_spec) if String === spec group, id, type, version, *rest = spec.split(":") unless rest.empty? classifier, version = version, rest.shift fail "Expecting project:id:type:version or project:id:type:classifier:version, found #{spec}" unless rest.empty? end fail "Missing file type for #{spec}" if type.blank? spec_to_hash :group=>group, :id=>id, :type=>type, :version=>version, :classifier=>classifier elsif Hash === spec spec = spec.clone fail "Missing group identifier for #{spec.inspect}" if spec[:group].blank? fail "Missing artifact identifier for #{spec.inspect}" if spec[:id].blank? fail "Missing version for #{spec.inspect}" if spec[:version].blank? spec[:type] = DEFAULT_FILE_TYPE if spec[:type].blank? spec else fail "Spec must be a string, hash or object that responds to to_spec" end end # Convert a hash back to a spec string. def hash_to_spec(hash) version = ":#{hash[:version]}" unless hash[:version].blank? classifier = ":#{hash[:classifier]}" unless hash[:classifier].blank? "#{hash[:group]}:#{hash[:id]}:#{hash[:type] || DEFAULT_FILE_TYPE}#{classifier}#{version}" end # Convert a hash to a file name. def hash_to_file_name(hash) version = "-#{hash[:version]}" unless hash[:version].blank? classifier = "-#{hash[:classifier]}" unless hash[:classifier].blank? "#{hash[:id]}#{version}#{classifier}.#{hash[:type] || DEFAULT_FILE_TYPE}" end end def initialize(*args) super enhance do |task| # Download the artifact form one of the remote repositories if the # file does not exist in the local repository, and no other behavior # specified. If this task has been enhanced, delegate to the other # enhancers. We don't know if this is the first or other enhancement, # but we only need to know it's not the only one. if @actions.size == 1 repositories.download(to_spec) end end end end # Singleton object for specifying the local, remote and release repositories. class Repositories include Singleton # Returns the path to the local repository. # # The default path is .m2/repository relative to the home directory. def local() @local ||= ENV["local_repo"] || File.join(ENV["HOME"], ".m2", "repository") end # Sets the path to the local repository. def local=(dir) @local = dir ? File.expand_path(dir) : nil end # Locates an artifact in the local repository based on its specification. # # For example: # locate :group=>"log4j", :id=>"log4j", :version=>"1.1" # => ~/.m2/repository/log4j/log4j/1.1/log4j-1.1.jar def locate(spec) spec = Artifact.spec_to_hash(spec) unless Hash === spec File.join(local, spec[:group].split("."), spec[:id], spec[:version], Artifact.hash_to_file_name(spec)) end # Returns a hash of all the remote repositories. The key is the repository # identifier, and the value is the repository base URL. def remote() @remote ||= {} end # Sets the remote repositories from a hash. See #remote. def remote=(hash) case hash when nil @remote = {} when Hash @remote = hash.clone else raise ArgumentError, "Expecting a hash" unless Hash === hash end end # Adds more remote repositories from a hash. See #remote. # # For example: # repositories.remote.add :ibiblio=>"http://www.ibiblio.org/maven2" def add(hash) remote.merge!(hash) end # Attempts to download the artifact from one of the remote repositories # and store it in the local repository. Returns the path if downloaded, # otherwise raises an exception. def download(spec) spec = Artifact.spec_to_hash(spec) unless Hash === spec path = locate(spec) puts "Downloading #{Artifact.hash_to_spec(spec)}" if Rake.application.options.trace return path if remote.any? do |repo_id, repo_url| begin rel_path = spec[:group].gsub(".", "/") + "/#{spec[:id]}/#{spec[:version]}/#{Artifact.hash_to_file_name(spec)}" Transports.perform URI.parse(repo_url.to_s) do |http| mkpath File.dirname(path), :verbose=>false http.download(rel_path, path) begin http.download(rel_path.ext("pom"), path.ext("pom")) rescue Transports::NotFound end end true rescue Exception=>error warn error if Rake.application.options.trace false end end fail "Failed to download #{Artifact.hash_to_spec(spec)}, tried the following repositories:\n#{repositories.remote.values.join("\n")}" end # Specifies deployment target for all artifacts generated by this project. # # For example: # repositories.deploy_to = "sftp://example.com/var/www/maven/" # or: # repositories.deploy_to = "sftp://john:secret@example.com/var/www/maven/" def deploy_to=(options) options = { :url=>options } unless Hash === options @deploy_to = options end def deploy_to() @deploy_to || {} end end # Returns a global object for setting local and remote repositories. # See Repositories. def repositories() Repositories.instance end # Creates a file task to download and install the specified artifact. # # The artifact specification can be a string or a hash. # The file task points to the artifact in the local repository. # # You can provide alternative behavior to create the artifact instead # of downloading it from a remote repository. # # For example, to specify an artifact: # artifact("log4j:log4j:jar:1.1") # # To use the artifact in a task: # unzip artifact("org.apache.pxe:db-derby:zip:1.2") # # To specify an artifact and the means for creating it: # download(artifact("dojo:dojo-widget:zip:2.0")=> # "http://download.dojotoolkit.org/release-2.0/dojo-2.0-widget.zip") def artifact(spec, &block) spec = Artifact.spec_to_hash(spec) unless task = Artifact.lookup(spec) task = Artifact.define_task(repositories.locate(spec)) Artifact.register(task) end task.apply_spec spec task.enhance(&block) if block_given? task end # Creates multiple artifacts from a set of specifications and returns # an array of tasks. # # You can pass any number of arguments, each of which can be: # * An artifact specification, string or hash. Returns a new task for # each specification, by calling #artifact. # * An artifact task or any other task. Returns the task as is. # * A project. Returns all packaging tasks in that project. # * An array of artifacts. Returns all the artifacts found there. # # This method handles arrays of artifacts as if they are flattend, # to help in managing large combinations of artifacts. For example: # xml = [ xerces, xalan, jaxp ] # ws = [ axis, jax-ws, jaxb ] # db = [ jpa, mysql, sqltools ] # base = [ xml, ws, db ] # artifacts(base, models, services) # # You can also pass tasks and project. This is particularly useful for # dealing with dependencies between projects that are part of the same # build. # # For example: # artifacts(base, models, services, module1, module2) # # When passing a project as argument, it expands that project to all # its packaging tasks. You can then use the resulting artifacts as # dependencies that will force these packages to be build inside the # project, without installing them in the local repository. def artifacts(*specs) specs.inject([]) do |set, spec| case spec when Hash set |= [artifact(spec)] when /:.*:.*:/ set |= [artifact(spec)] when String set |= [file(spec)] when Rake::Task set |= [spec] when Project set |= artifacts(spec.packages) when Array set |= artifacts(*spec) else fail "Invalid artifact specification: #{spec || 'nil'}" end set end end # Convenience method for defining multiple artifacts that belong # to the same version and group. Accepts multiple artifact identifiers # (or arrays of) followed by two has keys: # * :under -- The group identifier # * :version -- The version number # # For example: # group "xbean", "xbean_xpath", "xmlpublic", :under=>"xmlbeans", :version=>"2.1.0" # Or: # group %w{xbean xbean_xpath xmlpublic}, :under=>"xmlbeans", :version=>"2.1.0" def group(*args) hash = args.pop args.flatten.map { |id| artifact :group=>hash[:under], :version=>hash[:version], :id=>id } end # Creates and return a task that will deploy all the specified # artifacts and files. Specify the deployment server by passing it # as the last argument (must be a hash). # # For example: # deploy(*process.packages, :url=>"sftp://example.com/var/www/maven") def deploy(*args) # Where do we release to? if Hash === args.last options = args.pop else options = repositories.deploy_to options = { :url=>options.to_s } unless Hash === options end url = options[:url] options = options.reject { |k,v| k === :url } fail "Don't know where to release, specify a URL or use repositories.deploy_to = url|hash" if url.blank? # Arguments are artifacts and types so we depend on them. task(url=>args).enhance do |task| Transports.perform url, options do |session| args.each do |artifact| if artifact.respond_to?(:to_spec) # Upload artifact relative to base URL, need to create path before uploading. puts "Deploying #{artifact.to_spec}" if verbose spec = artifact.to_spec_hash path = spec[:group].gsub(".", "/") + "/#{spec[:id]}/#{spec[:version]}/" session.mkpath path session.upload artifact.to_s, path + Artifact.hash_to_file_name(spec) else # Upload artifact to URL. puts "Deploying #{artifact}" if verbose session.upload artifact, File.basename(artifact) end end end end end end