(ns leiningen.deploy "Build and deploy jar to remote repository." (:require [cemerick.pomegranate.aether :as aether] [leiningen.core.classpath :as classpath] [leiningen.core.main :as main] [leiningen.core.eval :as eval] [leiningen.core.user :as user] [leiningen.core.utils :as utils] [clojure.java.io :as io] [leiningen.pom :as pom] [leiningen.jar :as jar] [clojure.java.shell :as sh] [clojure.string :as string])) (defn- abort-message [message] (cond (re-find #"Return code is 405" message) (str message "\n" "Ensure you are deploying over SSL.") (re-find #"Return code is 401" message) (str message "\n" "See `lein help deploying` for an explanation of how" " to specify credentials.") :else message)) (defn add-auth-from-url [[id settings]] (let [url (utils/build-url id) user-info (and url (.getUserInfo url)) [username password] (and user-info (.split user-info ":"))] (if username [id (assoc settings :username username :password password)] [id settings]))) (defn add-auth-interactively [[id settings]] (if (or (and (:username settings) (some settings [:password :passphrase :private-key-file])) (:no-auth settings) (re-find #"(file|scp|scpexe)://" (:url settings))) [id settings] (do (when @utils/rebound-io? (main/abort "No credentials found for " id "(did you mean `lein deploy" "clojars`?)\nPassword prompts are not supported when ran" "after other (potentially)\ninteractive tasks.\nSee `lein" "help deploy` for an explanation of how to specify" "credentials.")) (print "No credentials found for" id) (when (not= "clojars" id) (print "(did you mean `lein deploy clojars`?)")) (println "\nSee `lein help deploying` for how to configure credentials" "to avoid prompts.") (print "Username: ") (flush) (let [username (read-line) console (System/console) password (if console (.readPassword console "%s" (into-array ["Password: "])) (do (println "LEIN IS UNABLE TO TURN OFF ECHOING, SO" "THE PASSWORD IS PRINTED TO THE CONSOLE") (print "Password: ") (flush) (read-line)))] [id (assoc settings :username username :password password)])))) ;; repo names must not contain path delimiters because they're used by ;; aether for form filenames (defn- sanitize-repo-name [name] (last (.split name "/"))) (defn repo-for [project name] (let [settings (merge (get (into {} (:repositories project)) name) (get (into {} (:deploy-repositories project)) name))] (-> [(sanitize-repo-name name) (or settings {:url name})] (classpath/add-repo-auth) (add-auth-from-url) (add-auth-interactively)))) (defn signing-args "Produce GPG arguments for signing a file." [file opts] (let [key-spec (if-let [key (:gpg-key opts)] ["--default-key" key])] `["--yes" "-ab" ~@key-spec "--" ~file])) (defn sign "Create a detached signature and return the signature file name." [file opts] (let [{:keys [err exit]} (apply user/gpg (signing-args file opts))] (when-not (zero? exit) (main/abort "Could not sign" (str file "\n" err (if err "\n") "\nSee `lein help gpg` for how to set up gpg.\n" "If you don't expect people to need to verify the " "authorship of your jar, you\ncan add `:sign-releases " "false` to the relevant `:deploy-repositories` entry."))) (str file ".asc"))) (defn signature-for-artifact [[coords artifact-file] opts] {(apply concat (update-in (apply hash-map coords) [:extension] #(str (or % "jar") ".asc"))) (sign artifact-file opts)}) (defn signatures-for-artifacts "Creates and returns the list of signatures for the artifacts needed to be signed." [artifacts sig-opts] (let [total (count artifacts)] (println "Need to sign" total "files with GPG") (doall (map-indexed (fn [idx [coords artifact-file :as artifact]] (printf "[%d/%d] Signing %s with GPG\n" (inc idx) total artifact-file) (flush) (signature-for-artifact artifact sig-opts)) artifacts)))) (defn sign-for-repo? "Generally sign artifacts for this repo?" [repo] (:sign-releases (second repo) true)) (defn signing-opts "Extract signing options map from a project." [project repo] (merge (:signing project) (:signing (second repo)))) (defn files-for [project repo] (let [signed? (sign-for-repo? repo) ;; If pom is put in "target/", :auto-clean true will remove it if the ;; jar is created afterwards. So make jar first, then pom. artifacts (merge (jar/jar project) {[:extension "pom"] (pom/pom project)}) sig-opts (signing-opts project repo)] (if (and signed? (not (.endsWith (:version project) "-SNAPSHOT"))) (reduce merge artifacts (signatures-for-artifacts artifacts sig-opts)) artifacts))) (defn warn-missing-metadata [project] (doseq [key [:description :license :url]] (when (or (nil? (project key)) (re-find #"FIXME" (str (project key)))) (main/warn "WARNING: please set" key "in project.clj.")))) (defn- in-branches [branches] (-> (sh/sh "git" "rev-parse" "--abbrev-ref" "HEAD") :out butlast string/join branches not)) (defn- extension [f] (if-let [[_ signed-extension] (re-find #"\.([a-z]+\.asc)$" f)] signed-extension (if (= "pom.xml" (.getName (io/file f))) "pom" (last (.split f "\\."))))) (defn classifier "The classifier is be located between the version and extension name of the artifact. See http://maven.apache.org/plugins/maven-deploy-plugin/examples/deploying-with-classifiers.html " [version f] (let [pattern (re-pattern (format "%s-(\\p{Alnum}*)\\.%s" version (extension f))) [_ classifier-of] (re-find pattern f)] (when-not (empty? classifier-of) classifier-of))) (defn- fail-on-empty-project [project] (when-not (:root project) (main/abort "Couldn't find project.clj, which is needed for deploy task"))) (defn ^:no-project-needed deploy "Deploy jar and pom to remote repository. The target repository will be looked up in :repositories in project.clj: :repositories [[\"snapshots\" \"https://internal.repo/snapshots\"] [\"releases\" \"https://internal.repo/releases\"] [\"alternate\" \"https://other.server/repo\"]] If you don't provide a repository name to deploy to, either \"snapshots\" or \"releases\" will be used depending on your project's current version. You may provide a repository URL instead of a name. See `lein help deploying` under \"Authentication\" for instructions on how to configure your credentials so you are not prompted on each deploy. You can also deploy arbitrary artifacts from disk: $ lein deploy myrepo com.blueant/fancypants 1.0.1 fancypants.jar pom.xml The repository can be defined in defproject or a profile, or it can be a URL. Use file://$HOME/.m2/repository to install in the local repo. While this works with any arbitrary files on disk, downstream projects will not be able to depend on jars that are deployed without a pom." ([project] (deploy project (if (pom/snapshot? project) "snapshots" "releases"))) ([project repository] (fail-on-empty-project project) (let [branches (set (:deploy-branches project))] (when (and (seq branches) (in-branches branches)) (apply main/abort "Can only deploy from branches listed in" ":deploy-branches:" branches))) (warn-missing-metadata project) (let [repo (repo-for project repository) files (files-for project repo)] (try (java.lang.System/setProperty "aether.checksums.forSignature" "true") (main/debug "Deploying" files "to" repo) (aether/deploy :coordinates [(symbol (:group project) (:name project)) (:version project)] :artifact-map files :transfer-listener :stdout :repository [repo]) (catch org.eclipse.aether.deployment.DeploymentException e (when main/*debug* (.printStackTrace e)) (main/abort (abort-message (.getMessage e))))))) ([project repository identifier version & files] (let [identifier (symbol identifier) artifact-id (name identifier) group-id (namespace identifier) repo (repo-for project repository) artifacts (for [f files] [[:extension (extension f) :classifier (classifier version f)] f])] (java.lang.System/setProperty "aether.checksums.forSignature" "true") (main/debug "Deploying" files "to" repo) (aether/deploy :coordinates [(symbol group-id artifact-id) version] :artifact-map (into {} artifacts) :transfer-listener :stdout :repository [repo] :local-repo (:local-repo project)))))