require "core/project" require "core/transports" module Buildr desc "Download all artifacts" task "artifacts" # Mixin with a task to make it behave like an artifact. Implemented by the packaging tasks. # # An artifact has an identifier, group identifier, type, version number and # optional classifier. All can be used to locate it in the local repository, # download from or upload to a remote repository. # # The #to_spec and #to_hash methods allow it to be used everywhere an artifact is # accepted. module ActsAsArtifact ARTIFACT_ATTRIBUTES = [:group, :id, :type, :classifier, :version] class << self private def included(mod) mod.extend self end end # The artifact identifier. attr_reader :id # The group identifier. attr_reader :group # The file type. (Symbol) attr_reader :type # The version number. attr_reader :version # Optional artifact classifier. attr_reader :classifier # :call-seq: # to_spec_hash() => Hash # # Returns the artifact specification as a hash. For example: # com.example:app:jar:1.2 # becomes: # { :group=>"com.example", # :id=>"app", # :type=>:jar, # :version=>"1.2" } 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 # :call-seq: # to_spec() => String # # Returns the artifact specification, in the structure: # ::: # or # :::<:version> def to_spec() classifier.blank? ? "#{group}:#{id}:#{type}:#{version}" : "#{group}:#{id}:#{type}:#{classifier}:#{version}" end # :call-seq: # pom() => Artifact # # Convenience method that returns a POM artifact. def pom() return self if type == :pom artifact(:group=>group, :id=>id, :version=>version, :type=>:pom, :classifier=>classifier) end protected # 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 end # A file task referencing an artifact in the local repository. # # This task includes all the artifact attributes (group, id, version, etc). It points # to the artifact's path in the local repository. When invoked, it will download the # artifact into the local repository if the artifact does not already exist. # # Note: You can enhance this task to create the artifact yourself, e.g. download it from # a site that doesn't have a remote repository structure, copy it from a different disk, etc. class Artifact < Rake::FileCreationTask # The default artifact type. DEFAULT_TYPE = :jar include ActsAsArtifact class << self # :call-seq: # lookup(spec) => Artifact # # Lookup a previously registered artifact task based on its specification (String or Hash). def lookup(spec) @artifacts ||= {} @artifacts[to_spec(spec)] end # :call-seq: # register(artifacts) => artifacts # # Register an artifact task(s) for later lookup (see #lookup). def register(*tasks) @artifacts ||= {} fail "You can only register an artifact task, one of the arguments is not a Task that responds to to_spec()" unless tasks.all? { |task| task.respond_to?(:to_spec) && task.respond_to?(:invoke) } tasks.each { |task| @artifacts[task.to_spec] = task } tasks end # :call-seq: # to_hash(spec_hash) => spec_hash # to_hash(spec_string) => spec_hash # to_hash(artifact) => spec_hash # # 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. 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].to_s if 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] = spec[:type].blank? ? DEFAULT_TYPE : spec[:type].to_sym 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 or , 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 # :call-seq: # to_spec(spec_hash) => spec_string # # Convert a hash back to a spec string. This method accepts # a string, hash or any object that responds to to_spec. 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_TYPE}#{classifier}#{version}" end # :call-seq: # hash_to_file_name(spec_hash) => file_name # # Convert a hash spec 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_TYPE}" end end def initialize(*args) #:nodoc: super enhance do |task| # 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. # For that, we need to be the last piece of code run by the task. task.enhance do unless Rake.application.options.dryrun || File.exist?(name) repositories.download(to_spec) end end end end end # Holds the path to the local repository, URLs for remote repositories, and # settings for the deployment repository. # # You can access this object from the #repositories method. For example: # puts repositories.local # repositories.remote << "http://example.com/repo" # repositories.deploy_to = "sftp://example.com/var/www/public/repo" class Repositories include Singleton # :call-seq: # local() => path # # 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 # :call-seq: # local = path # # Sets the path to the local repository. # # The best place to set the local repository path is from a buildr.rb file # located in your home directory. That way all your projects will share the same # path, without affecting other developers collaborating on these projects. def local=(dir) @local = dir ? File.expand_path(dir) : nil end # :call-seq: # locate(spec) => path # # Locates an artifact in the local repository based on its specification, and returns # a file path. # # 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 # :call-seq: # remote() => Array # # Returns an array of all the remote repository URLs. # # When downloading artifacts, repositories are accessed in the order in which they appear here. # The best way is to add repositories individually, for example: # repositories.remote << "http://example.com/repo" def remote() @remote ||= [] end # :call-seq: # remote = Array # remote = url # remote = nil # # With a String argument, clears the array and set it to that single URL. # # With an Array argument, clears the array and set it to these specific URLs. # # With nil, clears the array. def remote=(urls) case urls when nil @remote = nil when Array @remote = urls.dup else @remote = [urls.to_s] end end # :call-seq: # download(spec) => boolean # # Downloads an artifact from one of the remote repositories, and stores it in the local # repository. Accepts a String or Hash artifact specification, and returns a path to the # artifact in the local repository. Raises an exception if the artifact is not found. # # This method attempts to download the artifact from each repository in the order in # which they are returned from #remote, until successful. If you want to download an # artifact only if not already installed in the local repository, create an #artifact # task and invoke it directly. 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_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.join("\n")}" end # :call-seq: # deploy_to = url # deploy_to = hash # # Specifies the deployment repository. Accepts a Hash with different repository settings # (e.g. url, username, password), or a String to only set the repository URL. # # Besides the URL, all other settings depend on the transport protocol in use. See #Transports # for more details. Common settings include username and password. # # For example: # repositories.deploy_to = { :url=>"sftp://example.com/var/www/repo/", # :username="john", :password=>"secret" } # or: # repositories.deploy_to = "sftp://john:secret@example.com/var/www/repo/" def deploy_to=(options) options = { :url=>options } unless Hash === options @deploy_to = options end # :call-seq: # deploy_to() => hash # # Returns the current deployment repository setting as a Hash. This is a more convenient # way to specify the deployment repository, as it allows you to specify the settings # progressively. # # For example, the Rakefile will contain the repository URL used by all developers: # repositories.deploy_to[:url] ||= "sftp://example.com/var/www/repo" # Your private buildr.rb will contain your credentials: # repositories.deploy_to[:username] = "john" # repositories.deploy_to[:password] = "secret" def deploy_to() @deploy_to ||= {} end end # :call-seq: # repositories() => Repositories # # Returns an object you can use for setting the local repository path, remote repositories # URL and deployment repository settings. # # See Repositories. def repositories() Repositories.instance end # :call-seq: # artifact(spec) => Artifact # artifact(spec) { |task| ... } => Artifact # # Creates a file task to download and install the specified artifact in the local repository. # # You can use a String or a Hash for the artifact specification. The file task will point at # the artifact's path inside the local repository. You can then use this tasks as a prerequisite # for other tasks. # # This task will download and install the artifact only once. In fact, it will download and # install the artifact if the artifact does not already exist. You can enhance it if you have # a different way of creating the artifact in the local repository. See Artifact for more details. # # For example, to specify an artifact: # artifact("log4j:log4j:jar:1.1") # # To use the artifact in a task: # compile.with artifact("log4j:log4j:jar:1.1") # # 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) #:yields:task spec = Artifact.to_hash(spec) unless task = Artifact.lookup(spec) task = Artifact.define_task(repositories.locate(spec)) task.send :apply_spec, spec Rake::Task["rake:artifacts"].enhance [ task ] Artifact.register(task) end task.enhance &block end # :call-seq: # artifacts(*spec) => artifacts # # Handles multiple artifacts at a time. This method is the plural equivalent of # #artifacts, but can do more things. # # You can pass any number of arguments, each of which can be: # * An artifact specification (String or Hash). Returns the appropriate Artifact task. # * An artifact of any other task. Returns the task as is. # * A project. Returns all artifacts created (packaged) by that project. # * A string. Returns that string, assumed to be a file name. # * An array of artifacts. Calls #artifacts on the array, flattens the result. # # For example, handling a collection of artifacts: # xml = [ xerces, xalan, jaxp ] # ws = [ axis, jax-ws, jaxb ] # db = [ jpa, mysql, sqltools ] # artifacts(xml, ws, db) # # Using artifacts created by a project: # artifact project("my-app") # All packages # artifact project("mu-app").package(:war) # Only the WAR def artifacts(*specs) specs.inject([]) do |set, spec| case spec when Array set |= artifacts(*spec) when Hash set |= [artifact(spec)] when /([^:]+:){2,4}/ # A spec as opposed to a file name. set |= [artifact(spec)] when String # Must always expand path. set |= [File.expand_path(spec)] when Project set |= artifacts(spec.packages) when Rake::Task set |= [spec] else fail "Invalid artifact specification in: #{specs.inspect}" end end end # :call-seq: # groups(ids, :under=>group_name, :version=>number) => artifacts # # Convenience method for defining multiple artifacts that belong to the same group and version. # Accepts multiple artifact identifiers follows by two hash values: # * :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 # :call-seq: # deploy(*files) # deploy(*files, deploy_options) # # Deploys all the specified artifacts/files. If the last argument is a Hash, it is used to # specify the deployment repository. Otherwise, obtains the deployment repository by calling # Repositories#deploy_to. # # For example: # deploy(foo.packages, :url=>"sftp://example.com/var/www/repo") def deploy(*args) # Where do we release to? if Hash === args.last options = args.pop else options = repositories.deploy_to.clone options = { :url=>options.to_s } unless Hash === options end # Strip all options since the transport requires them separately from the URL. 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.flatten.each { |arg| arg.invoke if arg.respond_to?(:invoke) } Transports.perform url, options do |session| args.flatten.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.to_s, File.basename(artifact.to_s) end end end end end