lein-droid/lein-droid

0.3.1-SNAPSHOT


Plugin for easy Clojure/Android development and deployment

dependencies

robert/hooke
1.3.0
org.clojure/data.zip
0.1.1
de.ubercode.clostache/clostache
1.4.0



(this space intentionally left almost blank)
 

Clojure is simple. Android should also be.

This plugin is intended to make your Clojure/Android development as seamless and efficient as when developing ordinar Clojure JVM programs.

(ns leiningen.droid
  (:refer-clojure :exclude [compile doall repl])
  (:require clojure.pprint)
  (:use [leiningen.core.project :only [set-profiles]]
        [leiningen.core.main :only [abort]]
        [leiningen.help :only (subtask-help-for)]
        [leiningen.clean :only [clean]]
        [leiningen.droid.compile :only [compile code-gen]]
        [leiningen.droid
         [classpath :only [init-hooks]]
         [build :only [create-dex create-obfuscated-dex
                       crunch-resources package-resources create-apk
                       sign-apk zipalign-apk apk build jar]]
         [deploy :only [install run forward-port repl deploy]]
         [new :only [new init]]
         [compatibility :only [gather-dependencies]]
         [utils :only [proj wrong-usage android-parameters ensure-paths
                       dev-build?]]]))

Shows the list of possible lein droid subtasks.

(defn help
  ([]) ([droid-var]
          (println "lein-droid is a plugin for Clojure/Android development."
                   (subtask-help-for nil droid-var))))

Pretty-prints a representation of the project map.

(defn pprint
  [project & keys]
  (if (seq keys)
    (clojure.pprint/pprint (select-keys project (map read-string keys)))
    (clojure.pprint/pprint project))
  (flush))
(declare execute-subtask)

Metatask. Performs all Android tasks from compilation to deployment.

(defn doall
  [{{:keys [library]} :android :as project} & device-args]
  (let [build-steps (if library ["build"] ["build" "apk" "deploy"])
        build-steps (if (dev-build? project)
                      build-steps (cons "clean" build-steps))]
    (doseq [task build-steps]
      (execute-subtask project task device-args))))

DEPRECATED. Metatask. Builds, packs and deploys the release version of the project.

(defn release
  [project & args]
  (abort (str "\"release\" subtask is deprecated, "
              "please use 'lein with-profile release droid doall'")))

Supertask for Android-related tasks (see lein droid for list).

(defn ^{:no-project-needed true
        :subtasks [#'new #'init #'code-gen #'compile
                   #'create-dex #'create-obfuscated-dex
                   #'crunch-resources #'package-resources
                   #'create-apk #'sign-apk #'zipalign-apk
                   #'install #'run #'forward-port #'repl
                   #'build #'apk #'deploy #'doall #'release #'help
                   #'gather-dependencies #'jar #'pprint]}
  droid
  ([project]
     (help #'droid))
  ([project & [cmd & args]]
     (init-hooks)
     (some-> project
             android-parameters
             (execute-subtask cmd args))))

Executes a subtask defined by name on the given project.

(defn execute-subtask
  [project name args]
  (when (and (nil? project) (not (#{"new" "help" "init"} name)))
    (abort "Subtask" name "should be run from the project folder."))
  (case name
    ;; Standalone tasks
    "new" (if (< (count args) 2)
            (abort (wrong-usage "lein droid new" #'new))
            (apply new args))
    "init" (init (.getAbsolutePath (clojure.java.io/file ".")))
    "code-gen" (code-gen project)
    "compile" (compile project)
    "create-dex" (create-dex project)
    "create-obfuscated-dex" (create-obfuscated-dex project)
    "crunch-resources" (crunch-resources project)
    "package-resources" (package-resources project)
    "create-apk" (create-apk project)
    "sign-apk" (sign-apk project)
    "zipalign-apk" (zipalign-apk project)
    "install" (apply install project args)
    "run" (apply run project args)
    "forward-port" (apply forward-port project args)
    "repl" (repl project)
    "gather-dependencies" (apply gather-dependencies project args)
    "clean" (clean project)
    ;; Meta tasks
    "build" (build project)
    "apk" (apk project)
    "deploy" (apply deploy project args)
    "doall" (apply doall project args)
    "release" (apply release project args)
    "jar" (jar project)
    ;; Help tasks
    "pprint" (apply pprint project args)
    "help" (help #'droid)
    (println "Subtask is not recognized:" name
             (subtask-help-for nil #'droid))))
 

A set of functions and subtasks responsible for building the Android project.

(ns leiningen.droid.build
  (:refer-clojure :exclude [compile])
  (:use [leiningen.core
         [classpath :only [resolve-dependencies]]
         [main :only [debug info abort *debug*]]]
        [leiningen.droid
         [compile :only [code-gen compile]]
         [utils :only [get-sdk-android-jar sh dev-build?
                       ensure-paths with-process read-password append-suffix
                       create-debug-keystore get-project-file read-project
                       sdk-binary relativize-path get-sdk-support-jars
                       get-resource-jars]]
         [manifest :only [write-manifest-with-internet-permission]]])
  (:require [clojure.string]
            [clojure.set]
            [clojure.java.io :as io]
            [leiningen.droid.sdk :as sdk]
            leiningen.jar leiningen.javac)
  (:import java.io.File))

Build-related subtasks

Run dex on the given target which should be either directory with .class files or jar file, e.g. one produced by proguard.

Since the execution of dx takes a pretty lot of time we need to ensure that its subprocess will be killed if user cancels the build (sends SIGINT to leiningen). That is why we add a hook to the runtime that will be triggered when Leiningen is closed.

(defn- run-dx
  [{{:keys [sdk-path out-dex-path external-classes-paths
            force-dex-optimize dex-opts support-libraries]} :android,
            :as project}
   target]
  (let [dx-bin (sdk-binary project :dx)
        options (or dex-opts [])
        no-optimize (if (and (not force-dex-optimize) (dev-build? project))
                      "--no-optimize" [])
        annotations (str sdk-path "/tools/support/annotations.jar")
        deps (resolve-dependencies :dependencies project)
        support-jars (get-sdk-support-jars sdk-path support-libraries true)
        external-classes-paths (or external-classes-paths [])]
    (with-process [proc (map str
                             (flatten [dx-bin options "--dex" no-optimize
                                       "--output" out-dex-path
                                       target annotations deps
                                       support-jars
                                       external-classes-paths]))]
      (.addShutdownHook (Runtime/getRuntime) (Thread. #(.destroy proc))))))

Creates a DEX file from the compiled .class files.

(defn create-dex
  [{compile-path :compile-path :as project}]
  (info "Creating DEX....")
  (ensure-paths compile-path)
  (run-dx project compile-path))

Creates an obfuscated DEX file from the compiled .class files.

(defn create-obfuscated-dex
  [{{:keys [sdk-path out-dex-path external-classes-paths
            force-dex-optimize dex-opts target-version
            proguard-conf-path proguard-opts]} :android,
            compile-path :compile-path
            project-name :name
            target-path :target-path
            :as project}]
  (info "Creating obfuscated DEX....")
  (ensure-paths compile-path proguard-conf-path)
  (when-not (.isDirectory (io/file compile-path))
    (abort (format "compile-path (%s) is not a directory" compile-path)))
  (let [obfuscated-jar-file (str (io/file target-path
                                          (str project-name "-obfuscated.jar")))
        proguard-jar (sdk-binary project :proguard)
        android-jar (get-sdk-android-jar sdk-path target-version)
        proguard-opts (or proguard-opts [])
        annotations (str sdk-path "/tools/support/annotations.jar")
        deps (resolve-dependencies :dependencies project)
        external-paths (or external-classes-paths [])
        compile-path-dir (io/file compile-path)
        ;; to figure out what classes were thrown away by proguard
        orig-class-files
        (when *debug*
          (set (for [file (file-seq compile-path-dir)
                     :when (and (.isFile ^File file)
                                (.endsWith (str file) ".class"))]
                 (relativize-path compile-path-dir file))))]
    (sh "java"
        "-jar" proguard-jar
        (str "@" proguard-conf-path)
        "-injars" compile-path
        "-outjars" obfuscated-jar-file
        "-libraryjars" (->> (concat [annotations android-jar]
                                    deps external-paths)
                            (map str)
                            (clojure.string/join ":"))
        proguard-opts)
    (when *debug*
      (let [optimized-class-files
            (for [file (binding [*debug* false]
                         ;; Supress this output
                         (sh "jar" "tf" obfuscated-jar-file))
                  :let [trimmed (clojure.string/trim-newline file)]
                  :when (.endsWith ^String trimmed ".class")]
              trimmed)
            thrown-away-classes (clojure.set/difference orig-class-files
                                                        optimized-class-files)]
        (cond (empty? thrown-away-classes) nil
              (< (count thrown-away-classes) 30)
              (doseq [class thrown-away-classes]
                (debug class))
              :else
              (let [file (io/file target-path "removed-classes.txt")]
                (debug
                 (format "%s classes were removed by ProGuard. See list in %s."
                         (count thrown-away-classes) file))
                (spit file (clojure.string/join "\n" thrown-away-classes))))))
    (run-dx project obfuscated-jar-file)))

Updates the pre-processed PNG cache.

Calls aapt binary with the crunch task.

(defn crunch-resources
  [{{:keys [res-path out-res-path]} :android :as project}]
  (info "Crunching resources...")
  (ensure-paths res-path)
  (let [aapt-bin (sdk-binary project :aapt)]
    (sh aapt-bin "crunch -v"
        "-S" res-path
        "-C" out-res-path)))

We have to declare a future reference here because build and build-project-dependencies are mutually-recursive.

(declare build)

Builds all project dependencies for the current project.

(defn build-project-dependencies
  [{{:keys [project-dependencies]} :android, root :root}]
  (doseq [dep-path project-dependencies
          :let [dep-project (read-project (get-project-file root dep-path))]]
    (info "Building project dependency" dep-path "...")
    (build dep-project)
    (info "Building dependency complete.")))

Metatask. Builds dependencies, compiles and creates DEX (if not a library).

(defn build
  [{{:keys [library]} :android :as project}]
  (if library
    (doto project
      build-project-dependencies code-gen compile crunch-resources)
    (doto project
      build-project-dependencies code-gen compile create-dex)))

Metatask. Packages compiled Java files and Clojure sources into JAR.

Same as lein jar but appends Android libraries to the classpath while compiling Java files.

(defn jar
  [project]
  (leiningen.javac/javac project)
  (leiningen.jar/jar project))

APK-related subtasks

Packages application resources.

If this task is run with :dev profile, then it ensures that AndroidManifest.xml has Internet permission for running the REPL server. This is achieved by backing up the original manifest file and creating a new one with Internet permission appended to it. After the packaging the original manifest file is restored.

(defn package-resources
  [{{:keys [sdk-path target-version manifest-path assets-paths res-path
            out-res-path external-res-paths out-res-pkg-path
            rename-manifest-package assets-gen-path]} :android :as project}]
  (info "Packaging resources...")
  (ensure-paths sdk-path manifest-path res-path)
  (let [aapt-bin (sdk-binary project :aapt)
        android-jar (get-sdk-android-jar sdk-path target-version)
        dev-build (dev-build? project)
        debug-mode (if dev-build ["--debug-mode"] [])
        manifest-file (io/file manifest-path)
        backup-file (io/file (str manifest-path ".backup"))
        ;; Only add `assets` directory if it is present.
        assets (mapcat #(when (.exists (io/file %)) ["-A" %])
                       (conj assets-paths assets-gen-path))
        external-resources (for [res external-res-paths] ["-S" res])]
    (when dev-build
      (io/copy manifest-file backup-file)
      (write-manifest-with-internet-permission manifest-path))
    (sh aapt-bin "package" "--no-crunch" "-f" debug-mode "--auto-add-overlay"
        "-M" manifest-path
        "-S" out-res-path
        "-S" res-path
        external-resources
        assets
        "-I" android-jar
        "-F" out-res-pkg-path
        "--generate-dependencies"
        (if rename-manifest-package
          ["--rename-manifest-package" rename-manifest-package] []))
    (when dev-build
      (io/copy backup-file manifest-file)
      (io/delete-file backup-file))))

Creates a deployment-ready APK file.

It is done by executing methods from ApkBuilder SDK class on the generated DEX-file and the resource package.

(defn create-apk
  [{{:keys [out-apk-path out-res-pkg-path
            out-dex-path resource-jars-paths]} :android,
            java-only :java-only :as project}]
  (info "Creating APK...")
  (ensure-paths out-res-pkg-path out-dex-path)
  (let [suffix (if (dev-build? project) "debug-unaligned" "unaligned")
        unaligned-path (append-suffix out-apk-path suffix)
        resource-jars (concat (get-resource-jars project)
                              (map #(java.io.File. %) resource-jars-paths))]
    (sdk/create-apk project
                    :apk-name unaligned-path :resource-jars resource-jars)))

Signs APK file with the key taken from the keystore.

Either a debug keystore key or a release key is used based on whether the build type is the debug one. Creates a debug keystore if it is missing.

(defn sign-apk
  [{{:keys [out-apk-path sigalg
            keystore-path key-alias keypass storepass]} :android :as project}]
  (info "Signing APK with" keystore-path "...")
  (let [dev-build (dev-build? project)
        suffix (if dev-build "debug-unaligned" "unaligned")
        unaligned-path (append-suffix out-apk-path suffix)
        sigalg (or sigalg "SHA1withRSA")]
    (when (and dev-build (not (.exists (io/file keystore-path))))
      ;; Create a debug keystore if there isn't one
      (create-debug-keystore keystore-path))
    (ensure-paths unaligned-path keystore-path)
    (let [storepass     (or (when dev-build "android")
                            storepass
                            (System/getenv "STOREPASS")
                            (read-password "Enter storepass: "))
          keypass       (or (when dev-build "android")
                            keypass
                            (System/getenv "KEYPASS")
                            (read-password "Enter keypass: "))]
      (sh "jarsigner"
          "-sigalg" sigalg
          "-digestalg" "SHA1"
          "-keystore" keystore-path
          "-storepass" storepass
          "-keypass" keypass
          unaligned-path key-alias))))

Aligns resources locations on 4-byte boundaries in the APK file.

Done by calling zipalign binary on APK file.

(defn zipalign-apk
  [{{:keys [sdk-path out-apk-path]} :android :as project}]
  (info "Aligning APK...")
  (let [zipalign-bin (sdk-binary project :zipalign)
        unaligned-suffix (if (dev-build? project) "debug-unaligned" "unaligned")
        unaligned-path (append-suffix out-apk-path unaligned-suffix)
        aligned-path (if (dev-build? project)
                       (append-suffix out-apk-path "debug")
                       out-apk-path)]
    (ensure-paths unaligned-path)
    (.delete (io/file aligned-path))
    (sh zipalign-bin "4" unaligned-path aligned-path)))

Metatask. Crunches and packages resources, creates, signs and aligns an APK.

(defn apk
  [project]
  (doto project
    crunch-resources package-resources
    create-apk sign-apk zipalign-apk))
 

Contains functions and hooks for Android-specific classpath manipulation.

(ns leiningen.droid.classpath
  (:use [robert.hooke :only [add-hook]]
        [leiningen.droid.utils :only [get-sdk-android-jar
                                      get-sdk-google-api-jars
                                      get-sdk-support-jars]])
  (:import org.sonatype.aether.util.version.GenericVersionScheme))

Since dx and ApkBuilder utilities fail when they are feeded repeated jar-files, we need to make sure that JAR dependencies list contains only unique jars.

Filters project's dependency list for unique jars regardless of version or groupId. Android-patched version of Clojure is prefered over the other ones. For the rest the latest version is preferred.

(defn remove-duplicate-dependencies
  [dependencies]
  (let [tagged (for [[artifact version :as dep] dependencies]
                 (let [[_ group name] (re-matches #"(.+/)?(.+)" (str artifact))]
                   {:name name, :group group, :ver version, :original dep}))
        grouped (group-by :name tagged)
        scheme (GenericVersionScheme.)]
    (for [[name same-jars] grouped]
      ;; For Clojure jar choose only from Android-specific versions
      ;; (if there is at least one).
      (let [same-jars (if (= name "clojure")
                        (let [droid-clojures (filter #(= (:group %)
                                                         "org.clojure-android/")
                                                     same-jars)]
                          (if-not (empty? droid-clojures)
                            droid-clojures
                            same-jars))
                        same-jars)]
        (:original
         (reduce #(if (pos? (compare (.parseVersion scheme (or (:version %2)
                                                               "0"))
                                     (.parseVersion scheme (or (:version %1)
                                                               "0"))))
                    %2 %1)
                 same-jars))))))

Takes the original get-dependencies function and arguments to it. Removes duplicate entries from the result when resolving project dependencies.

(defn- dependencies-hook
  [f dependency-key project & rest]
  (let [all-deps (apply f dependency-key project rest)]
    (if (= dependency-key :dependencies)
      ;; aether/dependency-files expects a map but uses keys only,
      ;; so we transform a list into a map with nil values.
      (zipmap (remove-duplicate-dependencies (keys all-deps))
              (repeat nil))
      all-deps)))

We also have to manually attach Android SDK libraries to the classpath. The reason for this is that Leiningen doesn't handle external dependencies at the high level, and Android jars are not distributed in a convenient fashion (using Maven repositories). To solve this we hack into get-classpath function.

Takes the original get-classpath function and the project map, extracting the path to the Android SDK and the target version from it. Then the path to the actual android.jar file is constructed and appended to the rest of the classpath list.

(defn classpath-hook
  [f {{:keys [sdk-path target-version external-classes-paths
              use-google-api support-libraries]}
      :android :as project}]
  (let [classpath (f project)
        result (conj (concat classpath external-classes-paths
                             (when use-google-api
                               (get-sdk-google-api-jars sdk-path
                                                        target-version))
                             (get-sdk-support-jars sdk-path support-libraries))
                     (get-sdk-android-jar sdk-path target-version)
                     (str sdk-path "/tools/support/annotations.jar"))]
    result))
(defn init-hooks []
  (add-hook #'leiningen.core.classpath/get-dependencies #'dependencies-hook)
  (add-hook #'leiningen.core.classpath/get-classpath #'classpath-hook))
 

Contains utilities for letting lein-droid to cooperate with ant/Eclipse build tools.

(ns leiningen.droid.compatibility
  (:require [clojure.java.io :as io])
  (:use [leiningen.core
         [main :only [info]]
         [classpath :only [resolve-dependencies]]]
        [leiningen.droid.utils :only [ensure-paths]]))

Compatibility task. Copies the dependency libraries into the libs/ folder.

(defn gather-dependencies
  [{:keys [root] :as project} & {dir ":dir", :or {dir "libs"} :as other}]
  (println (class (first (keys other))))
  (info "Copying dependency libraries into" (str dir "..."))
  (let [destination-dir (io/file root dir)
        dependencies (resolve-dependencies :dependencies project)]
    (.mkdirs destination-dir)
    (doseq [dep dependencies]
      (io/copy dep
               (io/file destination-dir (.getName ^java.io.File dep))))))

Creates a file named .nrepl-port in project directory with port number inside, so that fireplace.vim can connect to the REPL.

(defn create-repl-port-file
  [{{:keys [repl-local-port]} :android, root :root}]
  (spit (io/file root ".nrepl-port") repl-local-port))
 

This part of the plugin is responsible for the project compilation.

(ns leiningen.droid.compile
  (:refer-clojure :exclude [compile])
  (:require [leiningen compile javac]
            [clojure.java.io :as io]
            [clojure.set :as sets]
            [leiningen.core.eval :as eval])
  (:use [leiningen.droid.utils :only [get-sdk-android-jar sdk-binary
                                      ensure-paths sh dev-build?]]
        [leiningen.droid.manifest :only [get-package-name generate-manifest]]
        [leiningen.core
         [main :only [debug info abort]]
         [classpath :only [get-classpath]]]
        [bultitude.core :only [namespaces-on-classpath]]))

Pre-compilation tasks

Save project's data-readers value to application's resources so it can be later retrieved in runtime. This is necessary to be able to use data readers when developing in REPL on the device.

(defn save-data-readers-to-resource
  [{{:keys [assets-gen-path]} :android :as project}]
  (.mkdirs (io/file assets-gen-path))
  (eval/eval-in-project
   project
   `(spit (io/file ~assets-gen-path "data_readers.clj")
          (into {} (map (fn [[k# v#]]
                          [k# (symbol (subs (str v#) 2))])
                        *data-readers*)))))

Generates the R.java file from the resources.

This task is necessary if you define the UI in XML and also to gain access to your strings and images by their ID.

(defn generate-resource-code
  [{{:keys [sdk-path target-version manifest-path res-path gen-path
            out-res-path external-res-paths library]} :android
    java-only :java-only :as project}]
  (info "Generating R.java...")
  (let [aapt-bin (sdk-binary project :aapt)
        android-jar (get-sdk-android-jar sdk-path target-version)
        manifest-file (io/file manifest-path)
        library-specific (if library "--non-constant-id" "--auto-add-overlay")
        external-resources (for [res external-res-paths] ["-S" res])]
    (ensure-paths manifest-path res-path android-jar)
    (.mkdirs (io/file gen-path))
    (.mkdirs (io/file out-res-path))
    (sh aapt-bin "package" library-specific "-f" "-m"
        "-M" manifest-path
        "-S" out-res-path
        "-S" res-path
        external-resources
        "-I" android-jar
        "-J" gen-path
        "--generate-dependencies"))
  project)

Generates R.java and builds a manifest with the appropriate version code and substitutions.

(defn code-gen
  [project]
  (doto project generate-manifest generate-resource-code))

Compilation

Stores a set of namespaces that should always be compiled regardless of the build type. Since these namespaces are used in eval-in-project call they naturally don't get AOT-compiled during automatic dependency resolution, so we have to make sure they are compiled anyway.

(def ^:private always-compile-ns
  '#{clojure.core clojure.core.protocols clojure.string
     clojure.java.io})

Takes project and returns a set of namespaces that should be AOT-compiled.

(defn namespaces-to-compile
  [{{:keys [aot aot-exclude-ns]} :android :as project}]
  (-> (case aot
        :all
          (seq (leiningen.compile/stale-namespaces (assoc project :aot :all)))
        :all-with-unused
          (namespaces-on-classpath :classpath
                                   (map io/file (get-classpath project)))
        ;; else
          (map symbol aot))
      set
      (sets/union always-compile-ns)
      (sets/difference (set (map symbol aot-exclude-ns)))))

Compiles Clojure files into .class files.

If :aot project parameter equals :all then compiles the necessary dependencies. If :aot equals :all-with-unused then compiles all namespaces of the dependencies whether they were referenced in the code or not. The latter is useful for the REPL-driven development.

Uses neko to set compilation flags. Some neko macros and subsequently project code depends on them to eliminate debug-specific code when building the release.

(defn compile-clojure
  [{{:keys [enable-dynamic-compilation start-nrepl-server
            manifest-path repl-device-port ignore-log-priority]}
    :android
    {:keys [nrepl-middleware]} :repl-options
    :as project}]
  (info "Compiling Clojure files...")
  (ensure-paths manifest-path)
  (debug "Project classpath:" (get-classpath project))
  (let [nses (namespaces-to-compile project)
        dev-build (dev-build? project)
        opts (cond-> {:neko.init/release-build (not dev-build)
                      :neko.init/start-nrepl-server start-nrepl-server
                      :neko.init/nrepl-port repl-device-port
                      :neko.init/enable-dynamic-compilation
                      enable-dynamic-compilation
                      :neko.init/ignore-log-priority ignore-log-priority
                      :neko.init/nrepl-middleware (list 'quote nrepl-middleware)
                      :neko.init/package-name (get-package-name manifest-path)}
                     (not dev-build) (assoc :elide-meta
                                       [:doc :file :line :column :added :arglists]))]
    (info (format "Build type: %s, dynamic compilation: %s, remote REPL: %s."
                  (if dev-build "debug" "release")
                  (if (or dev-build start-nrepl-server
                          enable-dynamic-compilation)
                    "enabled" "disabled")
                  (if (or dev-build start-nrepl-server) "enabled" "disabled")))
    (let [form
          `(binding [*compiler-options* ~opts]
             (doseq [namespace# '~nses]
               (println "Compiling" namespace#)
               (clojure.core/compile namespace#)))
          project (update-in project [:prep-tasks]
                             (partial remove #{"compile"}))]
      (.mkdirs (io/file (:compile-path project)))
      (try (eval/eval-in-project project form)
           (info "Compilation succeeded.")
           (catch Exception e
             (abort "Compilation failed."))))))

Compiles both Java and Clojure source files.

(defn compile
  [{{:keys [sdk-path gen-path]} :android, java-only :java-only :as project} & args]
  (ensure-paths sdk-path)
  (let [project (update-in project [:java-source-paths] conj gen-path)]
    (when-not java-only
      (save-data-readers-to-resource project))
    (apply leiningen.javac/javac project args)
    (when-not java-only
      (compile-clojure project))))
 

Functions and subtasks that install and run the application on the device and manage its runtime.

(ns leiningen.droid.deploy
  (:use [leiningen.core.main :only [debug info abort *debug*]]
        [leiningen.droid.manifest :only (get-launcher-activity
                                         get-package-name)]
        [leiningen.droid.utils :only [sh ensure-paths dev-build? append-suffix
                                      prompt-user sdk-binary]]
        [leiningen.droid.compatibility :only (create-repl-port-file)]
        [reply.main :only (launch-nrepl)]))

Returns the list of currently attached devices.

(defn- device-list
  [adb-bin]
  (let [output (rest (sh adb-bin "devices"))] ;; Ignore the first line
    (remove nil?
            (map #(let [[_ serial type] (re-find #"([^\t]+)\t([^\t]+)" %)]
                    (when serial
                      {:serial serial, :type type}))
                 output))))

If there is only one device attached returns its serial number, otherwise prompts user to choose the device to work with. If no devices are attached aborts the execution.

(defn- choose-device
  [adb-bin]
  (let [devices (device-list adb-bin)]
    (case (count devices)
      0 (abort "No devices are attached.")
      1 (:serial (first devices))
      (do
        (dotimes [i (count devices)]
          (println (format "%d. %s\t%s" (inc i) (:serial (nth devices i))
                           (:type (nth devices i)))))
        (print (format "Enter the number 1..%d to choose the device: "
                       (count devices)))
        (flush)
        (if-let [answer (try (Integer/parseInt (read-line))
                             (catch Exception ex))]
          (:serial (nth devices (dec answer)))
          (abort "Cannot recognize device number."))))))

Returns a list of adb arguments that specify the device adb should be working against. Calls choose-device if device-args parameter is nil.

(defn get-device-args
  [adb-bin device-args]
  (or device-args
      (list "-s" (choose-device adb-bin))))

Messages which adb install prints as the result.

(def ^{:doc 
       :private true}
  adb-responses
  {"Success" :success
   "Failure [INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES]"
   :inconsistent-certificates})
(def ^:private uninstall-prompt
  (str "Certificates of the installed application and the application being "
       "installed mismatch.\nDo you want to uninstall the old application "
       "first? (y/n): "))

Installs the APK on the only (or specified) device or emulator.

Since adb command always returns exit code zero, we have to manually parse its output to figure out what is going on. This is why this subtask is full of low-level stuff.

(defn install
  [{{:keys [out-apk-path manifest-path]} :android :as project}
   & device-args]
  (info "Installing APK...")
  (let [adb-bin (sdk-binary project :adb)
        apk-path (if (dev-build? project)
                   (append-suffix out-apk-path "debug")
                   out-apk-path)
        _ (ensure-paths apk-path)
        device (get-device-args adb-bin device-args)
        output (java.io.StringWriter.)]
    ;; Rebind *out* to get the output `adb` produces.
    (binding [*out* output, *debug* true]
      (sh adb-bin device "install" "-r" apk-path))
    (let [output (str output)
          response (some
                     adb-responses
                     (.split output (System/getProperty "line.separator")))]
      (case response
        :success (debug output)
        :inconsistent-certificates
        (let [resp (prompt-user uninstall-prompt)]
          (if (.equalsIgnoreCase "y" resp)
            (do
              (sh adb-bin device "uninstall" (get-package-name manifest-path))
              (sh adb-bin device "install" apk-path))
            (abort "Cannot proceed with installation.")))
        (do (info output)
            (abort "Abort execution."))))))

Launches the installed APK on the connected device.

(defn run
  [{{:keys [manifest-path launch-activity]} :android :as project}
   & device-args]
  (ensure-paths manifest-path)
  (when-let [activity (or launch-activity (get-launcher-activity project))]
    (info "Launching APK...")
    (let [adb-bin (sdk-binary project :adb)
          device (get-device-args adb-bin device-args)]
      (sh adb-bin device "shell" "am" "start" "-n" activity))))

Binds a port on the local machine to the port on the device.

This allows to connect to the remote REPL from the current machine.

(defn forward-port
  [{{:keys [repl-device-port repl-local-port]} :android :as project}
   & device-args]
  (info "Binding device port" repl-device-port
        "to local port" repl-local-port "...")
  (create-repl-port-file project)
  (let [adb-bin (sdk-binary project :adb)
        device (get-device-args adb-bin device-args)]
    (sh adb-bin device "forward"
        (str "tcp:" repl-local-port)
        (str "tcp:" repl-device-port))))

Connects to a remote nREPL server on the device using REPLy.

(defn repl
  [{{:keys [repl-local-port]} :android}]
  (launch-nrepl {:attach (str "localhost:" repl-local-port)}))

Metatask. Runs install,run,forward-port`.

(defn deploy
  [project & device-args]
  (let [adb-bin (sdk-binary project :adb)
        device (get-device-args adb-bin device-args)]
    (apply install project device)
    (apply run project device)
    (apply forward-port project device)))
 

Contains functions to manipulate AndroidManifest.xml file

(ns leiningen.droid.manifest
  (:require [clojure.data.zip.xml :refer :all]
            [clojure.xml :as xml]
            [clojure.java.io :as jio]
            [clojure.zip :refer [append-child node up xml-zip]]
            [clostache.parser :as clostache]
            [leiningen.core.main :refer [info]]
            [leiningen.release :refer [parse-semantic-version]])
  (:import (java.io FileWriter)))

Constants

Name of the category for the launcher activities.

(def ^{:private true} launcher-category "android.intent.category.LAUNCHER")

Name of the Internet permission.

(def ^{:private true} internet-permission "android.permission.INTERNET")

XML tag of the Internet permission.

(def ^{:private true} internet-permission-tag
  {:tag :uses-permission
   :attrs {(keyword :android:name) internet-permission}})

Attribute name for target SDK version.

(def ^:private target-sdk-attribute (keyword :android:targetSdkVersion))

Attribute name for minimal SDK version.

(def ^:private min-sdk-attribute (keyword :android:minSdkVersion))

Attribute name for project version name.

(def ^:private version-name-attribute (keyword :android:versionName))

Local functions

Parses given XML manifest file and creates a zipper from it.

(defn- load-manifest
  [manifest-path]
  (xml-zip (xml/parse manifest-path)))

Returns a list of zipper trees of Activities which belong to the launcher category.

(defn- get-all-launcher-activities
  [manifest]
  (xml-> manifest :application :activity :intent-filter :category
         (attr= :android:name launcher-category)))

Checks if manifest contains Internet permission.

(defn- has-internet-permission?
  [manifest]
  (first (xml-> manifest
                :uses-permission (attr= :android:name internet-permission))))

Writes the manifest to the specified filename.

(defn- write-manifest
  [manifest filename]
  (binding [*out* (FileWriter. filename)]
    (xml/emit (node manifest))))

Public functions

Returns the name of the application's package.

(defn get-package-name
  [manifest-path]
  (first (xml-> (load-manifest manifest-path) (attr :package))))

Returns the package-qualified name of the first activity from the manifest that belongs to the launcher category.

(defn get-launcher-activity
  [{{:keys [manifest-path rename-manifest-package]} :android}]
  (let [manifest (load-manifest manifest-path)
        [activity-name] (some-> manifest
                            get-all-launcher-activities
                            first
                            up up
                            (xml-> (attr :android:name)))
        pkg-name (first (xml-> manifest (attr :package)))]
    (when activity-name
      (str (or rename-manifest-package pkg-name) "/"
           (str pkg-name activity-name)))))

Updates the manifest on disk guaranteed to have the Internet permission.

(defn write-manifest-with-internet-permission
  [manifest-path]
  (let [manifest (load-manifest manifest-path)]
   (write-manifest (if (has-internet-permission? manifest)
                     manifest
                     (append-child manifest internet-permission-tag))
                   manifest-path)))

Extracts the target SDK version from the provided manifest file. If target SDK is not specified returns minimal SDK.

(defn get-target-sdk-version
  [manifest-path]
  (let [[uses-sdk] (xml-> (load-manifest manifest-path) :uses-sdk)
        [target-sdk] (xml-> uses-sdk (attr target-sdk-attribute))]
    (or target-sdk
        (first (xml-> uses-sdk (attr min-sdk-attribute))))))

Extracts the project version name from the provided manifest file.

(defn get-project-version
  [manifest-path]
  (first (xml-> (load-manifest manifest-path) (attr version-name-attribute))))
(def ^:private version-bit-sizes [9 9 9 5])
(def ^:private version-maximums
  (mapv (partial bit-shift-left 1) version-bit-sizes))
(def ^:private version-coefficients
  (mapv (fn [offset] (bit-shift-left 1 (- 32 offset)))
        (reductions + version-bit-sizes)))

Asserts that a>b in version segments

(defn- assert>
  [a b]
  (assert (> a b) (str "Version number segment too large to fit in the
  version-code scheme " b ">" a ", maximum version in each segment
  is " (clojure.string/join "." version-maximums)))
  b)

Given a version map containing :major :minor :patch :build and :priority version numbers, returns an integer which is guaranteed to be greater for semantically larger version numbers.

Splitting the 32 bit version code into 5 segments such that each semantically greater version will have a larger version code. The segments represent major, minor, patch, build and package priority (multiple builds of the same android apk where one takes precedence over another, for instance in the case where higher resolution assets are available, but a fallback is made available for devices which do not support the configuration).

Largest possible version number: v512.512.512 (32)

(defn version-code
  [version-map]
  (->> version-map
       ((juxt :major :minor :patch :priority))
       (map (fnil assert> 0 0) version-maximums)
       (map * version-coefficients)
       (reduce +)))

If a :manifest-template-path is specified, perform template substitution with the values in :android :manifest, including the version-name and version-code which are automatically generated, placing the output in :manifest-path.

(defn generate-manifest
  [{{:keys [manifest-path manifest-template-path manifest-options target-path
            build-type]} :android, version :version :as project}]
  (info "Generating manifest...")
  (let [full-manifest-map (merge {:version-name version
                                  :version-code (-> version
                                                    parse-semantic-version
                                                    version-code)
                                  :debug-build (not build-type)}
                                 manifest-options)]
    (when (.exists (jio/file manifest-template-path))
      (clojure.java.io/make-parents manifest-path)
      (->> full-manifest-map
           (clostache/render (slurp manifest-template-path))
           (spit manifest-path)))))
 

Provides tasks for creating a new project or initialiaing plugin support in an existing one.

(ns leiningen.droid.new
  (:require [clojure.string :as string]
            [clojure.java.io :as io])
  (:use [leiningen.core.main :only [info abort]]
        [leiningen.new.templates :only [render-text slurp-resource
                                        sanitize ->files]]
        [leiningen.droid.manifest :only [get-target-sdk-version
                                         get-project-version]]))

Taken from lein-newnew.

Create a renderer function that looks for mustache templates in the right place given the name of your template. If no data is passed, the file is simply slurped and the content returned unchanged.

(defn renderer
  [name]
  (fn [template & [data]]
    (let [res (io/resource (str name "/" (sanitize template)))]
      (if data
        (render-text (slurp-resource res) data)
        (io/input-stream res)))))
(defn package-to-path [package-name]
  (string/replace package-name #"\." "/"))

Loads a properties file. Returns nil if the file doesn't exist.

(defn- load-properties
  [file]
  (when (.exists file)
    (with-open [rdr (io/reader file)]
      (let [properties (java.util.Properties.)]
        (.load properties rdr)
        properties))))
(defn package-name-valid? [package-name]
  (and (not (.startsWith package-name "."))
       (> (.indexOf package-name ".") -1)
       (= (.indexOf package-name "-") -1)))

Creates project.clj file in an existing Android project folder.

Presumes default directory names (like src, res and gen) and AndroidManifest.xml file to be already present in the project.

(defn init
  [current-dir]
  (let [manifest (io/file current-dir "AndroidManifest.xml")]
    (when-not (.exists manifest)
      (abort "ERROR: AndroidManifest.xml not found - have to be in an existing"
             "Android project. Use `lein droid new` to create a new project."))
    (let [manifest-path (.getAbsolutePath manifest)
          [_ name] (re-find #".*/(.+)/\." current-dir)
          props (load-properties (io/file current-dir "project.properties"))
          data {:name name
                :version (or (get-project-version manifest-path)
                             "0.0.1-SNAPSHOT")
                :target-sdk (or (get-target-sdk-version manifest-path) "10")
                :library? (if (and props
                                   (= (.getProperty props "android.library")
                                      "true"))
                            ":library true" "")}
          render (renderer "templates")]
      (info "Creating project.clj...")
      (io/copy (render "library.project.clj" data)
               (io/file current-dir "project.clj")))))

Creates new Android project given the project's name and package name.

(defn new
  [project-name package-name & options]
  (when-not (package-name-valid? package-name)
    (abort "ERROR: Package name should have at least two levels and"
           "not contain hyphens (you can replace them with underscores)."))
  (let [options (apply hash-map options)
        data {:name project-name
              :package package-name
              :package-sanitized (sanitize package-name)
              :path (package-to-path (sanitize package-name))
              :activity (get options ":activity" "MainActivity")
              :target-sdk (get options ":target-sdk" "15")
              :app-name (get options ":app-name" project-name)}
        render (renderer "templates")]
    (info "Creating project" project-name "...")
    (->files
     data
     "assets"
     [".gitignore" (render "gitignore")]
     ["LICENSE" (render "LICENSE" data)]
     ["AndroidManifest.template.xml" (render "AndroidManifest.template.xml" data)]
     ["project.clj" (render "project.clj" data)]
     ["res/drawable-hdpi/splash_circle.png" (render "splash_circle.png")]
     ["res/drawable-hdpi/splash_droid.png" (render "splash_droid.png")]
     ["res/drawable-hdpi/splash_hands.png" (render "splash_hands.png")]
     ["res/drawable-hdpi/ic_launcher.png" (render "ic_launcher_hdpi.png")]
     ["res/drawable-mdpi/ic_launcher.png" (render "ic_launcher_mdpi.png")]
     ["res/drawable-ldpi/ic_launcher.png" (render "ic_launcher_ldpi.png")]
     ["res/drawable/splash_background.xml" (render "splash_background.xml")]
     ["res/anim/splash_rotation.xml" (render "splash_rotation.xml")]
     ["res/layout/splashscreen.xml" (render "splashscreen.xml")]
     ["res/values/strings.xml" (render "strings.xml" data)]
     ["src/java/{{path}}/SplashActivity.java" (render "SplashActivity.java" data)]
     ["src/clojure/{{path}}/main.clj" (render "main.clj" data)])))
 

Functions to interact with Android SDK tools.

(ns leiningen.droid.sdk
  (:use [leiningen.core.main :only [debug]])
  (:require [cemerick.pomegranate :as pomegranate]
            [clojure.java.io :as io])
  (:import java.io.File java.io.PrintStream))

Uses reflection to make an ApkBuilder instance.

(defn- make-apk-builder
  [apk-name res-path dex-path]
  (let [apkbuilder-class (Class/forName "com.android.sdklib.build.ApkBuilder")
        constructor (. apkbuilder-class getConstructor
                       (into-array [File File File String PrintStream]))]
    (.newInstance constructor (into-array [(io/file apk-name) (io/file res-path)
                                           nil nil nil]))))

Returns paths to unpacked native libraries if they exist, nil otherwise.

(defn- get-unpacked-natives-paths
  []
  (let [path "target/native/linux/"]
    (when (.exists (io/file path))
      [path])))

Delegates APK creation to ApkBuilder class in sdklib.jar.

(defn create-apk
  [{{:keys [sdk-path out-res-pkg-path out-dex-path native-libraries-paths]}
    :android} & {:keys [apk-name resource-jars]}]
  ;; Dynamically load sdklib.jar
  (pomegranate/add-classpath (io/file sdk-path "tools" "lib" "sdklib.jar"))
  (let [apkbuilder (make-apk-builder apk-name out-res-pkg-path out-dex-path)
        all-native-libraries (concat native-libraries-paths
                                     (get-unpacked-natives-paths))
        dexes (filter #(re-matches #".*dex" (.getName %))
                      (.listFiles (io/file out-dex-path)))]
    (when (seq resource-jars)
      (debug "Adding resource libraries: " resource-jars)
      (doseq [rj resource-jars]
        (.addResourcesFromJar apkbuilder rj)))
    (when (seq all-native-libraries)
      (debug "Adding native libraries: " all-native-libraries)
      (doseq [lib all-native-libraries]
        (.addNativeLibraries apkbuilder ^File (io/file lib))))
    (when (seq dexes)
      (debug "Adding DEX files: " dexes)
      (doseq [dex dexes]
        (.addFile apkbuilder dex (.getName dex))))
    (.sealApk apkbuilder)))
 

Provides utilities for the plugin.

(ns leiningen.droid.utils
  (:require [leiningen.core.project :as pr])
  (:use [clojure.java.io :only (file reader)]
        [leiningen.core.main :only (info debug abort)]
        [leiningen.core.classpath :only [resolve-dependencies]]
        [clojure.string :only (join)])
  (:import java.io.File))

Convenient functions to run SDK binaries

Checks if the given directories or files exist. Aborts Leiningen execution in case either of them doesn't or the value equals nil.

We assume paths to be strings or lists/vectors. The latter case is used exclusively for Windows batch-files which are represented like cmd.exe /C batch-file, so we test third element of the list for the existence.

(defmacro ensure-paths
  [& paths]
  `(do
     ~@(for [p paths]
         `(cond (nil? ~p)
                (abort "The value of" (str '~p) "is nil. Abort execution.")
                (or
                 (and (sequential? ~p) (not (.exists (file (nth ~p 2)))))
                 (and (string? ~p) (not (.exists (file ~p)))))
                (abort "The path" ~p "doesn't exist. Abort execution.")))))

Returns true if we are running on Microsoft Windows

(defn windows?
  []
  (= java.io.File/separator "\\"))

Returns a map of relative paths to different SDK binaries for both Unix and Windows platforms.

(defn sdk-binary-paths
  [sdk-path build-tools-version]
  (ensure-paths sdk-path)
  (let [bt-root-dir (file sdk-path "build-tools")
        ;; build-tools directory contains a subdir which name we don't
        ;; know that has all the tools. Let's grab the first directory
        ;; inside build-tools/ and hope it is the one we need.
        bt-dir (or build-tools-version
                   (->> (.list bt-root-dir)
                        (filter #(.isDirectory (file bt-root-dir %)))
                        sort last)
                   (abort "Build tools not found."
                          "Download them using the Android SDK Manager."))
        bt-ver (Integer/parseInt (get (re-find #"(\d+)\..*" bt-dir) 1 "-1"))]
    ;; if bt-ver is non-negative we have a definite numeric version number
    ;; assume the latest build-tools dir is not empty
    {:dx {:unix ["build-tools" bt-dir "dx"]
          :win ["build-tools" bt-dir "dx.bat"]}
     :adb {:unix ["platform-tools" "adb"]
           :win ["platform-tools" "adb.exe"]}
     :aapt {:unix ["build-tools" bt-dir "aapt"]
            :win ["build-tools" bt-dir "aapt.exe"]}
     :zipalign (if (>= bt-ver 20)
                 {:unix ["build-tools" bt-dir "zipalign"]
                  :win ["build-tools" bt-dir "zipalign.exe"]}
                 {:unix ["tools" "zipalign"]
                  :win ["tools" "zipalign.exe"]})
     :proguard {:unix ["tools" "proguard" "lib" "proguard.jar"]
                :win ["tools" "proguard" "lib" "proguard.jar"]}}))

Given the project map and the binary keyword, returns either a full path to the binary as a string, or a vector with call to cmd.exe for batch-files.

(defn sdk-binary
  [{{:keys [sdk-path build-tools-version]} :android} binary-kw]
  (let [binary (get-in (sdk-binary-paths sdk-path build-tools-version)
                       [binary-kw (if (windows?) :win :unix)])
        binary-str (str (apply file sdk-path binary))]
    (ensure-paths binary-str)
    (if (.endsWith (last binary) ".bat")
      ["cmd.exe" "/C" binary-str]
      binary-str)))

Middleware section

Taken from Leiningen source code.

Absolutizes the path given root if it is relative. Leaves the path as is if it is absolute.

(defn absolutize
  [root path]
  (str (if (.isAbsolute (file path))
         path
         (file root path))))

Taken from Leiningen source code.

Absolutizes all values with keys ending with path or paths in the :android map of the project.

(defn absolutize-android-paths
  [{:keys [root android] :as project}]
  (assoc project :android
         (into {} (for [[key val] android]
                    [key (cond (re-find #"-path$" (name key))
                               (absolutize root val)
                               (re-find #"-paths$" (name key))
                               (map (partial absolutize root) val)
                               :else val)]))))

Returns a map of the default android-specific parameters.

(defn get-default-android-params
  [{root :root, name :name, target-path :target-path
    java-paths :java-source-paths}]
  (let [manifest-template "AndroidManifest.template.xml"
        manifest-template-file (file (absolutize root manifest-template))
        gen-path (str (file target-path "gen"))
        has-template (.exists manifest-template-file)]
    {:out-dex-path target-path
     :manifest-path (if has-template
                      (str (file target-path "AndroidManifest.xml"))
                      "AndroidManifest.xml")
     :manifest-template-path manifest-template
     :manifest-options {:app-name "@string/app_name"}
     :res-path "res"
     :gen-path gen-path
     :out-res-path (str (file target-path "res"))
     :assets-paths ["assets"]
     :assets-gen-path (str (file target-path "assets-gen"))
     :out-res-pkg-path (str (file target-path (str name ".ap_")))
     :out-apk-path (str (file target-path (str name ".apk")))
     :keystore-path (str (file (System/getProperty "user.home")
                               ".android" "debug.keystore"))
     :key-alias "androiddebugkey"
     :repl-device-port 9999
     :repl-local-port 9999
     :target-version 10}))
(declare android-parameters)

Reads and initializes a Leiningen project and applies Android middleware to it.

(defn read-project
  [project-file]
  (android-parameters (pr/init-project (pr/read (str project-file)))))

Returns the path to project.clj file in the specified project directory (either absolute or relative).

(defn get-project-file
  [root project-directory-path]
  (let [project-directory (file project-directory-path)]
    (if (.isAbsolute project-directory)
      (file project-directory-path "project.clj")
      (file root project-directory-path "project.clj"))))

Parses project.clj files from the project dependencies to extract the paths to external resources and class files.

(defn process-project-dependencies
  [{{:keys [project-dependencies]} :android, root :root :as project}]
  (reduce (fn [project dependency-path]
            (let [project-file (get-project-file root dependency-path)]
              (if-not (.exists ^File project-file)
                (do
                  (info "WARNING:" (str project-file) "doesn't exist.")
                  project)
                (let [dep (read-project project-file)
                      {:keys [compile-path dependencies]} dep
                      {:keys [res-path out-res-path]} (:android dep)]
                  (-> project
                      (update-in [:dependencies]
                                 concat dependencies)
                      (update-in [:android :external-classes-paths]
                                 conj compile-path)
                      (update-in [:android :external-res-paths]
                                 conj res-path out-res-path))))))
          project project-dependencies))

Merges project's :android map with the default parameters map, processes project dependencies and absolutizes paths in the :android map.

This is the middleware function to be plugged into project.clj.

(defn android-parameters
  [{:keys [android] :as project}]
  (let [android-params (merge (get-default-android-params project)
                              android)]
    (-> project
        (assoc :android android-params)
        process-project-dependencies
        absolutize-android-paths)))

General utilities

(defn proj [] (read-project "sample/project.clj"))

If version keyword is passed (for example, :ics or :jelly-bean), resolves it to the version number. Otherwise just returns the input.

(defn sdk-version-number
  [kw-or-number]
  (if (keyword? kw-or-number)
    (case kw-or-number
      :ics         15
      :jelly-bean  18
      :kitkat      19
      :lollipop    21
      (abort "Unknown Android SDK version: " kw-or-number))
    kw-or-number))

Returns a version-specific path to the Android platform tools.

(defn get-sdk-platform-path
  [sdk-root version]
  (str (file sdk-root "platforms" (str "android-"
                                       (sdk-version-number version)))))

Returns a version-specific path to the android.jar SDK file.

(defn get-sdk-android-jar
  [sdk-root version]
  (str (file (get-sdk-platform-path sdk-root version) "android.jar")))

Returns a version-specific path to the Google SDK directory.

(defn get-sdk-google-api-path
  [sdk-root version]
  (str (file sdk-root "add-ons" (str "addon-google_apis-google-"
                                     (sdk-version-number version)))))

Returns a version-specific paths to all Google SDK jars.

(defn get-sdk-google-api-jars
  [sdk-root version]
  (map #(.getAbsolutePath ^File %)
       (rest ;; The first file is the directory itself, no need in it.
        (file-seq
         (file (str (get-sdk-google-api-path sdk-root version) "/libs"))))))

Returns a path to the Android Support library.

(defn- get-sdk-support-jar
  [sdk-root version]
  (.getAbsolutePath
   (apply file sdk-root "extras" "android" "support"
          (case version
            "v4"             ["v4" "android-support-v4.jar"]
            "v7-appcompat"   ["v7" "appcompat" "libs"
                              "android-support-v7-appcompat.jar"]
            "v7-gridlayout"  ["v7" "gridlayout" "libs"
                              "android-support-v7-gridlayout.jar"]
            "v7-mediarouter" ["v7" "mediarouter" "libs"
                              "android-support-v7-mediarouter.jar"]
            "v13"            ["v13" "android-support-v13.jar"]
            (abort "Unknown support library version in :support-libraries : "
                   version)))))

Takes a list of support library versions and returns a list of JAR files.

(defn get-sdk-support-jars
  [sdk-root version-list & [warn?]]
  (let [message "WARNING: Support library V4 is redundant if you use V13."
        versions (set version-list)
        versions (if (every? versions ["v4" "v13"])
                   (do (when warn? (info message))
                       (disj versions "v4"))
                   versions)]
    (map #(get-sdk-support-jar sdk-root %) (seq versions))))

Get the list of dependency libraries that has :use-resources true in their definition.

(defn get-resource-jars
  [{:keys [dependencies] :as project}]
  (let [res-deps (for [[lib _ & options :as dep] (:dependencies project)
                       :when (or (:use-resources (apply hash-map options))
                                 ;; Should be removed in final release
                                 (= lib 'org.clojure-android/clojure))]
                   dep)
        mod-proj (assoc project :dependencies res-deps)]
    (resolve-dependencies :dependencies mod-proj)))

Executes the subprocess specified in the binding list and applies body do it while it is running. The binding list consists of a var name for the process and the list of strings that represents shell command.

After body is executed waits for a subprocess to finish, then checks the exit code. If code is not zero then prints the subprocess' output. If in DEBUG mode print both the command and it's output even for the successful run.

(defmacro with-process
  [[process-name command] & body]
  `(do
     (apply debug ~command)
     (let [builder# (ProcessBuilder. ~command)
           _# (.redirectErrorStream builder# true)
           ~process-name (.start builder#)
           output# (line-seq (reader (.getInputStream ~process-name)))]
       ~@body
       (.waitFor ~process-name)
       (doseq [line# output#]
         (if (= (.exitValue ~process-name) 0)
           (debug line#)
           (info line#)))
       (when-not (= (.exitValue ~process-name) 0)
         (abort "Abort execution."))
       output#)))

Executes the command given by args in a subprocess. Flattens the given list.

(defn sh
  [& args]
  (with-process [process (flatten args)]))

Checks the build type of the current project, assuming dev build if not a release build

(defn dev-build?
  [project]
  (not= (get-in project [:android :build-type]) :release))

Returns a string with the information about the proper function usage.

(defn wrong-usage
  ([task-name function-var]
     (wrong-usage task-name function-var 0))
  ([task-name function-var arglist-number]
     (let [arglist (-> function-var
                       meta :arglists (nth arglist-number))
           argcount (count arglist)
           parametrify #(str "<" % ">")
           ;; Replace the destructuring construction after & with
           ;; [optional-args].
           arglist (if (= (nth arglist (- argcount 2)) '&)
                     (concat (map parametrify
                                  (take (- argcount 2) arglist))
                             ["[optional-args]"])
                     (map parametrify arglist))]
       (format "Wrong number of argumets. USAGE: %s %s"
               task-name (join (interpose " " arglist))))))

Reads a string from the console until the newline character.

(defn prompt-user
  [prompt]
  (print prompt)
  (flush)
  (read-line))

Reads the password from the console without echoing the characters.

(defn read-password
  [prompt]
  (if-let [console (System/console)]
    (join (.readPassword console prompt nil))
    (prompt-user prompt)))

Appends a suffix to a filename, e.g. transforming sample.apk into sample-signed.apk

(defn append-suffix
  [filename suffix]
  (let [[_ without-ext ext] (re-find #"(.+)(\.\w+)" filename)]
    (str without-ext "-" suffix ext)))

Creates a keystore for signing debug APK files.

(defn create-debug-keystore
  [keystore-path]
  (sh "keytool" "-genkey" "-v"
      "-keystore" keystore-path
      "-alias" "androiddebugkey"
      "-sigalg" "SHA1withRSA"
      "-keyalg" "RSA"
      "-keysize" "1024"
      "-validity" "365"
      "-keypass" "android"
      "-storepass" "android"
      "-dname" "CN=Android Debug,O=Android,C=US"))
(defn relativize-path [^File dir ^File to-relativize]
  (.getPath (.relativize (.toURI dir)
                         (.toURI to-relativize))))