module Buildr # This module gives you a way to access the individual properties of an # artifact: id, group, type, classifier and version. It also provides other # methods commonly used on an artifact, specifically #to_hash and #to_spec. module ActsAsArtifact ARTIFACT_ATTRIBUTES = [:group, :id, :type, :classifier, :version] class << self def included(mod) mod.extend self end end # The artifact identifier. attr_reader :id # The group identifier. attr_reader :group # The file type. attr_reader :type # The version number. attr_reader :version # Optional artifact 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: # <group>:<artifact>:<type>:<version> # or # <group>:<artifact>:<type>:<classifier><:<version> def to_spec() classifier.blank? ? "#{group}:#{id}:#{type}:#{version}" : "#{group}:#{id}:#{type}:#{classifier}:#{version}" end # Apply specification to this artifact. def apply_spec(spec) spec = Artifact.to_hash(spec) ARTIFACT_ATTRIBUTES.each { |key| instance_variable_set("@#{key}", spec[key]) } self end # Convenience method that returns a POM artifact. def pom() return self if type.to_s == "pom" artifact(:group=>group, :id=>id, :version=>version, :type=>"pom", :classifier=>classifier) end end # The Artifact task maps to an artifact file in the local repository # and knows how to download the file from a remote repository. # # The task will only download the file if it does not exist. You can # enhance the task to create the artifact yourself. class Artifact < Rake::FileCreationTask # The default file type for artifacts, if not specified. DEFAULT_FILE_TYPE = "jar" include ActsAsArtifact class << self # Lookup a previously registered artifact task based on the # artifact specification (string or hash). def lookup(spec) @artifacts ||= {} @artifacts[to_spec(spec)] end # Register an artifact task(s) for later lookup (see #lookup). def register(*tasks) @artifacts ||= {} fail "You can only register an artifact task, strings and hashes are just not good enough" unless tasks.all? { |task| task.respond_to?(:to_spec) && task.respond_to?(:invoke) } tasks.each { |task| @artifacts[task.to_spec] = task } tasks 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 to use this # method: # * You can pass anything that could possibly be a spec, and get a hash. # * It will check that the spec includes the group identifier, artifact # identifier and version number and set the file type, if missing. # * It will always return a new specs hash. # # :nodoc: def to_hash(spec) if spec.respond_to?(:to_spec) to_hash spec.to_spec elsif Hash === spec # Sanitize the hash and check it's valid. spec = ARTIFACT_ATTRIBUTES.inject({}) { |h, k| h[k] = spec[k] ; h } 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 elsif String === spec group, id, type, version, *rest = spec.split(":") unless rest.empty? # Optional classifier comes before version. classifier, version = version, rest.shift fail "Expecting <project:id:type:version> or <project:id:type:classifier:version>, found <#{spec}>" unless rest.empty? end to_hash :group=>group, :id=>id, :type=>type, :version=>version, :classifier=>classifier else fail "Expecting a String, Hash or object that responds to to_spec" end end # Convert a hash back to a spec string. This method accepts # a string, hash or any object that responds to to_spec. # :nodoc: def to_spec(hash) hash = to_hash(hash) unless Hash === 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. # :nodoc: 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 execute() # Default behavior: download the artifact from one of the remote # repositories if the file does not exist. But this default behavior # is counter useful if the artifact knows how to build itself # (e.g. download from a different location), so don't perform it # if the task found a different way to create the artifact. super unless Rake.application.options.dryrun || File.exist?(name) repositories.download(to_spec) end end end # Holds the path to the local repository, URLs for remote repositories, and # settings for the deployment repository. class Repositories include Singleton # Returns the path to the local repository. # # The default path is .m2/repository relative to the home directory. # You can change the location of the local repository by using a symbol # link or by setting a different path. If you set a different path, do it # in the buildr.rb file instead of the Rakefile. 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.to_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.to_hash(spec) unless Hash === spec path = locate(spec) puts "Downloading #{Artifact.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.to_spec(spec)}, tried the following repositories:\n#{repositories.remote.values.join("\n")}" end # Specifies the deployment repository. Accepts a hash with the different # repository settings (e.g. url, username, password). Anything else is # interepted as the URL. # # For example: # repositories.deploy_to = { :url=>"sftp://example.com/var/www/maven/", # :username="john", :password=>"secret" } # 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 # Returns the current deployment repository configuration. This is a more # convenient way to specify deployment in the Rakefile, and override it # locally. For example: # # Rakefile # repositories.deploy_to[:url] ||= "sftp://example.com" # # buildr.rb # repositories.deploy_to[:url] = "sftp://acme.org" def deploy_to() @deploy_to ||= {} end end # Returns a global object for setting local, remote and deploy 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.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 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 Project set |= artifacts(spec.packages) when Rake::Task set |= [spec] when Array set |= artifacts(*spec) else fail "Invalid artifact specification: #{spec.to_s || '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 # Deploys all the specified artifacts/files. Specify the deployment # server by passing a hash as the last argument, or have it use # repositories.deploy_to. # # 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 deploy, perhaps you forgot to set repositories.deploy_to" if url.blank? args.each { |arg| arg.invoke if arg.respond_to?(:invoke) } 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