lein-droid/lein-droid0.4.1-SNAPSHOTPlugin for easy Clojure/Android development and deployment dependencies
| (this space intentionally left almost blank) | |||||||||||||||
(ns lein-droid.plugin (:require [clojure.java.io :refer [file]] [leiningen.core.main :refer [abort]] [leiningen.droid.utils :refer [ensure-paths]])) | ||||||||||||||||
Lein-droid's middleware adds Android SDK local repositories to :repositories. It has to be done in middleware because artifacts from that repositories are in :dependencies section, and other Leiningen tasks will crash. | (defn middleware [project] (let [sdk-path (file (get-in project [:android :sdk-path])) p (fn [& path] {:url (str "file://" (apply file sdk-path path))})] (ensure-paths sdk-path) (update-in project [:repositories] concat [["android-support" (p "extras" "android" "m2repository")] ["android-play-services" (p "extras" "google" "m2repository")]]))) | |||||||||||||||
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 [leiningen.droid.aar :refer [extract-aar-dependencies]] [leiningen.droid.code-gen :refer [code-gen]]) (: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]] [leiningen.droid [classpath :only [init-hooks]] [build :only [create-dex crunch-resources package-resources create-apk sign-apk zipalign-apk apk build jar aar]] [deploy :only [install run forward-port repl deploy local-repo]] [new :only [new init]] [test :only [local-test]] [utils :only [proj wrong-usage android-parameters ensure-paths dev-build?]]])) | |||||||||||||||
Shows the list of possible | (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"])] (doseq [task build-steps] (execute-subtask project task device-args)))) | |||||||||||||||
Supertask for Android-related tasks (see | (defn ^{:no-project-needed true :subtasks [#'new #'init #'code-gen #'compile #'create-dex #'crunch-resources #'package-resources #'create-apk #'sign-apk #'zipalign-apk #'install #'run #'forward-port #'repl #'build #'apk #'deploy #'doall #'help #'local-test #'jar #'pprint]} droid ([project] (help #'droid)) ([project & [cmd & args]] (init-hooks) (when (and (nil? project) (not (#{"new" "help" "init"} cmd))) (abort "Subtask" cmd "should be run from the project folder.")) (doto (android-parameters project) extract-aar-dependencies (execute-subtask cmd args)))) | |||||||||||||||
Executes a subtask defined by | (defn execute-subtask [project name args] (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) "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) "clean" (clean project) "local-repo" (local-repo project) ;; Test tasks "local-test" (apply local-test project args) ;; Meta tasks "build" (build project) "apk" (apk project) "deploy" (apply deploy project args) "doall" (apply doall project args) "jar" (jar project) "aar" (aar project) ;; Help tasks "pprint" (apply pprint project args) "help" (help #'droid) (println "Subtask is not recognized:" name (subtask-help-for nil #'droid)))) | |||||||||||||||
Utilities for manipulating Android package format (AAR). | (ns leiningen.droid.aar (:require [clojure.java.io :as io] [clojure.edn :as edn] [leiningen.core.classpath :as cp] [leiningen.core.main :refer [debug]]) (:import java.io.File net.lingala.zip4j.core.ZipFile)) | |||||||||||||||
Returns a list of artifact dependencies that have | (defn- get-aar-dependencies [project] (let [deps (cp/get-dependencies :dependencies project)] (for [[[art-id ver & opts :as dep]] deps :let [opts (apply hash-map opts)] :when (= (:extension opts) "aar")] dep))) | |||||||||||||||
Takes a dependency vector and returns its stringified version to be used in a file system. | (defn- str-dependency [dep] (-> (meta dep) :dependency .getArtifact str (.replace ":" "_"))) | |||||||||||||||
Unpacks all AAR dependencies of the project into the target directory. | (defn extract-aar-dependencies [{:keys [target-path] :as project}] (let [deps (set (get-aar-dependencies project)) aar-extracted-dir (io/file target-path "aar-extracted") ;; Read which AARs we already extracted to avoid doing it again. extracted-file (io/file aar-extracted-dir "extracted.edn") already-extracted (when (.exists ^File extracted-file) (edn/read-string (slurp extracted-file)))] (when-not (or (empty? deps) (= deps already-extracted)) (debug "Extracting AAR dependencies: " deps) (doseq [dep deps] (.extractAll (ZipFile. (:file (meta dep))) (str (io/file aar-extracted-dir (str-dependency dep))))) (spit extracted-file deps)))) | |||||||||||||||
Returns the list of files or directories specified by | (defn get-aar-files [{:keys [target-path] :as project} & subpath] (let [aar-extracted-dir (io/file target-path "aar-extracted")] (for [dep (get-aar-dependencies project)] (apply io/file aar-extracted-dir (str-dependency dep) subpath)))) | |||||||||||||||
Returns the list of all jars extracted from all AAR dependencies. | (defn get-aar-classes [project] (let [classes-jars (get-aar-files project "classes.jar") libs-dirs (get-aar-files project "libs")] (concat (filter #(.exists ^File %) classes-jars) (mapcat #(.listFiles ^File %) libs-dirs)))) | |||||||||||||||
Returns the list of existing paths to native libraries extracted from AAR dependencies. | (defn get-aar-native-paths [project] (filter #(.exists ^File %) (get-aar-files project "jni"))) | |||||||||||||||
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 [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-annotations-jar get-resource-jars get-sdk-build-tools-path]]]) (:require [clojure.string :as str] [clojure.set :as set] [clojure.java.io :as io] leiningen.core.project [leiningen.droid [code-gen :refer [code-gen]] [aar :refer [get-aar-files]] [manifest :refer [get-package-name]] [sdk :as sdk]] [leiningen.jar :as jar] leiningen.javac leiningen.pom) (:import java.io.File net.lingala.zip4j.core.ZipFile net.lingala.zip4j.model.ZipParameters net.lingala.zip4j.util.Zip4jConstants)) | |||||||||||||||
Build-related subtasks | ||||||||||||||||
Run proguard on the compiled classes and dependencies, create an JAR with minimized and shaken classes. | (defn- run-proguard-minifying [{{:keys [external-classes-paths proguard-conf-path proguard-opts proguard-output-jar-path]} :android compile-path :compile-path :as project}] (info "Running Proguard...") (ensure-paths compile-path proguard-conf-path) (let [proguard-bin (sdk-binary project :proguard) android-jar (get-sdk-android-jar project) annotations (get-sdk-annotations-jar project) deps (resolve-dependencies :dependencies project) external-paths (or external-classes-paths []) proguard-opts (or proguard-opts [])] (sh proguard-bin (str "@" proguard-conf-path) "-injars" (->> (concat [compile-path] deps external-paths) (map str) (str/join ":")) "-libraryjars" (->> [annotations android-jar] (map str) (str/join ":")) "-outjars" proguard-output-jar-path proguard-opts))) | |||||||||||||||
Run proguard on the compiled classes and dependencies to determine which classes have to be kept in primary dex. | (defn- run-proguard-multidexing [{{:keys [multi-dex-proguard-conf-path multi-dex-root-classes-path]} :android :as project} target-paths] (ensure-paths multi-dex-proguard-conf-path) (let [proguard-bin (sdk-binary project :proguard) android-jar (io/file (get-sdk-build-tools-path project) "lib" "shrinkedAndroid.jar")] (sh proguard-bin (str "@" multi-dex-proguard-conf-path) "-injars" (str/join ":" target-paths) "-libraryjars" (str android-jar) "-outjars" multi-dex-root-classes-path))) | |||||||||||||||
Creates a text file with the list of classes that should be included into primary dex. | (defn- generate-main-dex-list [{{:keys [multi-dex-root-classes-path multi-dex-main-dex-list-path]} :android :as project} target-paths] (run-proguard-multidexing project target-paths) (let [dx-jar (io/file (get-sdk-build-tools-path project) "lib" "dx.jar") builder (ProcessBuilder. ["java" "-cp" (str dx-jar) "com.android.multidex.MainDexListBuilder" multi-dex-root-classes-path (str/join ":" target-paths)]) process-name (.start builder) output (line-seq (io/reader (.getInputStream process-name))) writer (io/writer (io/file multi-dex-main-dex-list-path))] (binding [*out* writer] (doseq [line output] (println line))) (.waitFor process-name))) | |||||||||||||||
Run dex on the given target paths, each should be either a directory with .class files or a jar file. Since the execution of | (defn- run-dx [{{:keys [out-dex-path force-dex-optimize dex-opts multi-dex multi-dex-root-classes-path multi-dex-main-dex-list-path]} :android :as project} target-paths] (if multi-dex (do (info "Creating multi DEX....") (generate-main-dex-list project target-paths)) (info "Creating DEX....")) (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 (get-sdk-annotations-jar project) multi-dex (if multi-dex ["--multi-dex" "--main-dex-list" multi-dex-main-dex-list-path] [])] (with-process [proc (->> [dx-bin options "--dex" no-optimize multi-dex "--output" out-dex-path target-paths annotations] flatten (map str))] (.addShutdownHook (Runtime/getRuntime) (Thread. #(.destroy proc)))))) | |||||||||||||||
Creates a DEX file from the compiled .class files. | (defn create-dex [{{:keys [sdk-path external-classes-paths proguard-execute proguard-output-jar-path]} :android, compile-path :compile-path :as project}] (if proguard-execute (do (run-proguard-minifying project) (run-dx project proguard-output-jar-path)) (let [deps (resolve-dependencies :dependencies project) external-classes-paths (or external-classes-paths [])] (run-dx project (concat [compile-path] deps external-classes-paths))))) | |||||||||||||||
Metatask. Compiles the project and creates DEX. | (defn build [{{:keys [library]} :android :as project}] (doto project code-gen compile create-dex)) | |||||||||||||||
Metatask. Packages compiled Java files and Clojure sources into JAR. Same as | (defn jar [project] (leiningen.javac/javac project) (jar/write-jar project (jar/get-jar-filename project) (#'jar/filespecs (dissoc project :java-source-paths)))) | |||||||||||||||
Metatask. Packages library into AAR archive. | (defn aar [{{:keys [manifest-path res-path gen-path assets-paths]} :android :keys [name version target-path compile-path root] :as project}] (code-gen project) (.renameTo (io/file gen-path "R.txt") (io/file target-path "R.txt")) (leiningen.javac/javac project) ;; Remove unnecessary files (.delete ^File (io/file gen-path "R.java.d")) (let [package-name (get-package-name manifest-path) classes-path (apply io/file compile-path (str/split package-name #"\."))] (doseq [^File file (.listFiles ^File classes-path) :let [filename (.getName file)] :when (or (= filename "R.class") (re-matches #"R\$\w+\.class" filename))] (.delete file))) ;; Make a JAR (jar/write-jar project (io/file target-path "classes.jar") (#'jar/filespecs (dissoc project :java-source-paths))) ;; Finally create AAR file (let [zip (ZipFile. (io/file target-path (format "%s-%s.aar" name version))) params (doto (ZipParameters.) (.setCompressionMethod Zip4jConstants/COMP_STORE) ;; (.setDefaultFolderPath "target") (.setEncryptFiles false))] (.addFile zip (io/file manifest-path) params) (.addFile zip (io/file target-path "classes.jar") params) (.addFile zip (io/file target-path "R.txt") params) (.addFolder zip (io/file res-path) params) (when (.exists (io/file root "libs")) (.addFolder zip (io/file root "libs") params)) (doseq [path assets-paths :when (.exists (io/file path))] (.addFolder zip (io/file path) params))) (leiningen.pom/pom (assoc project :packaging "aar"))) | |||||||||||||||
APK-related subtasks | ||||||||||||||||
Updates the pre-processed PNG cache. Calls Because of the AAPT bug we must turn paths to files here so that proper canonical names are calculated. | (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) crunch (fn [src-dir target-dir] (sh aapt-bin "crunch -v" "-S" src-dir "-C" target-dir))] (doseq [aar (get-aar-files project) :when (.exists ^File (io/file aar "R.txt")) :let [out (io/file aar "out-res")]] (.mkdirs ^File out) (crunch (io/file aar "res") out)) (crunch (io/file res-path) (io/file out-res-path)))) | |||||||||||||||
Packages application resources. | (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) debug-mode (if (dev-build? project) ["--debug-mode"] []) ;; Only add `assets` directories when they are present. assets (mapcat #(when (.exists (io/file %)) ["-A" (str %)]) (concat assets-paths [assets-gen-path] (get-aar-files project "assets"))) aar-resources (for [res (get-aar-files project "res")] ["-S" res]) aar-crunched-resources (for [res (get-aar-files project "out-res") :when (.exists ^File res)] ["-S" res]) external-resources (for [res external-res-paths] ["-S" res])] (sh aapt-bin "package" "--no-crunch" "-f" debug-mode "--auto-add-overlay" "-M" manifest-path "-S" out-res-path "-S" res-path aar-crunched-resources aar-resources external-resources assets "-I" android-jar "-F" out-res-pkg-path "--generate-dependencies" (if rename-manifest-package ["--rename-manifest-package" rename-manifest-package] [])))) | |||||||||||||||
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 resource-jars-paths]} :android :as project}] (info "Creating APK...") (ensure-paths out-res-pkg-path) (let [unaligned-path (append-suffix out-apk-path "unaligned") resource-jars (concat (get-resource-jars project) (map 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 use-debug-keystore keystore-path key-alias keypass storepass]} :android :as project}] (info "Signing APK with" keystore-path "...") (let [debug (or (dev-build? project) use-debug-keystore) unaligned-path (append-suffix out-apk-path "unaligned") sigalg (or sigalg "SHA1withRSA")] (when (and debug (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 debug "android") storepass (System/getenv "STOREPASS") (read-password "Enter storepass: ")) keypass (or (when debug "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 | (defn zipalign-apk [{{:keys [sdk-path out-apk-path]} :android :as project}] (info "Aligning APK...") (let [zipalign-bin (sdk-binary project :zipalign) unaligned-path (append-suffix out-apk-path "unaligned")] (ensure-paths unaligned-path) (.delete (io/file out-apk-path)) (sh zipalign-bin "4" unaligned-path out-apk-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 (:require [leiningen.droid.aar :refer [get-aar-classes]] [leiningen.droid.utils :refer [get-sdk-android-jar get-sdk-annotations-jar]] [robert.hooke :refer [add-hook]]) (:import org.sonatype.aether.util.version.GenericVersionScheme)) | |||||||||||||||
Since | ||||||||||||||||
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 | (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))) | |||||||||||||||
Takes the original | (defn- resolve-dependencies-hook [f dependency-key project & rest] (let [deps (apply f dependency-key project rest)] (if (= dependency-key :dependencies) (concat deps (get-aar-classes project)) 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 | ||||||||||||||||
Takes the original | (defn classpath-hook [f {{:keys [external-classes-paths]} :android :as project}] (let [classpath (f project) result (conj (concat classpath external-classes-paths) (get-sdk-android-jar project) (get-sdk-annotations-jar project))] result)) | |||||||||||||||
(defn init-hooks [] (add-hook #'leiningen.core.classpath/get-dependencies #'dependencies-hook) (add-hook #'leiningen.core.classpath/resolve-dependencies #'resolve-dependencies-hook) (add-hook #'leiningen.core.classpath/get-classpath #'classpath-hook)) | ||||||||||||||||
Tasks and related functions for build-specific code generation. | (ns leiningen.droid.code-gen (:require [clojure.java.io :as io] [clojure.string :as str] [clostache.parser :as clostache] [leiningen.core.main :refer [debug info abort]] [leiningen.droid.aar :refer [get-aar-files]] [leiningen.droid.manifest :refer [get-package-name generate-manifest]] [leiningen.droid.sideload :as sideload] [leiningen.droid.utils :refer [get-sdk-android-jar sdk-binary ensure-paths sh dev-build?]] [leiningen.new.templates :refer [slurp-resource]]) (:import java.io.File)) | |||||||||||||||
BuildConfig.java generation | ||||||||||||||||
Mapping of classes to type strings as they should appear in BuildConfig. | (defn- java-type [x] (condp = (type x) Boolean "boolean" String "String" Long "long" Double "double" (abort ":build-config only supports boolean, String, long and double types."))) | |||||||||||||||
Transform a map of constants return to form readable by Clostache. | (defn map-constants [constants] (map (fn [[k v]] (binding [*print-dup* true] {:key k :value (pr-str v) :type (java-type v)})) constants)) | |||||||||||||||
(defn generate-build-constants [{{:keys [manifest-path gen-path build-config rename-manifest-package]} :android, version :version :as project}] (ensure-paths manifest-path) (let [res (io/resource "templates/BuildConfig.java") package-name (get-package-name manifest-path) gen-package-path (apply io/file gen-path (str/split package-name #"\.")) application-id (or rename-manifest-package package-name) template-constants (-> (merge {"VERSION_NAME" version "APPLICATION_ID" application-id} build-config) map-constants)] (ensure-paths gen-package-path) (->> {:debug (dev-build? project) :package-name package-name :constants template-constants} (clostache/render (slurp-resource res)) (spit (io/file gen-package-path "BuildConfig.java"))))) | ||||||||||||||||
R.java generation | ||||||||||||||||
Generates R.java file given full symbols file, library symbols file and library package name. Symbols file are loaded from respective R.txt files. | (defn create-r-file [full-symbols lib-r-txt lib-package gen-path] (debug "Generating R.java file for:" lib-package) (let [symbols (sideload/symbol-loader lib-r-txt) writer (sideload/symbol-writer (str gen-path) lib-package full-symbols)] (.load symbols) (.addSymbolsToWrite writer symbols) (.write writer))) | |||||||||||||||
Generates R.java files for the project and all dependency libraries, having R.txt for project and each library. | (defn generate-r-files [{{:keys [sdk-path gen-path manifest-path]} :android :as project}] (sideload/sideload-jars sdk-path) (let [full-r-txt (io/file gen-path "R.txt") full-symbols (sideload/symbol-loader full-r-txt)] (.load full-symbols) (dorun (map (fn [manifest, ^File r-txt] (when (.exists r-txt) (let [package-name (get-package-name manifest) lib-gen-path gen-path] (create-r-file full-symbols r-txt package-name lib-gen-path)))) (get-aar-files project "AndroidManifest.xml") (get-aar-files project "R.txt"))))) | |||||||||||||||
Generates R.java files for both the project and the libraries. | (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 files...") (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"] []) aar-resources (for [res (get-aar-files project "res")] ["-S" (str res)]) 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 aar-resources external-resources "-I" android-jar "-J" gen-path "--output-text-symbols" gen-path "--auto-add-overlay" "--generate-dependencies") ;; Finally generate R.java files having R.txt keys (when-not library (generate-r-files project)))) | |||||||||||||||
Generates R.java and builds a manifest with the appropriate version code and substitutions. | (defn code-gen [{{:keys [library]} :android :as project}] (doto project generate-manifest generate-resource-code generate-build-constants)) | |||||||||||||||
This part of the plugin is responsible for the project compilation. | (ns leiningen.droid.compile (:refer-clojure :exclude [compile]) (:require [bultitude.core :as bultitude] [clojure.java.io :as io] [clojure.set :as set] [leiningen.compile :refer [stale-namespaces]] [leiningen.core.classpath :refer [get-classpath]] [leiningen.core.eval :as eval] [leiningen.core.main :refer [debug info abort]] [leiningen.droid.manifest :refer [get-package-name]] [leiningen.droid.utils :refer [ensure-paths dev-build?]] leiningen.javac) (:import java.util.regex.Pattern)) | |||||||||||||||
Pre-compilation tasks | ||||||||||||||||
(defn eval-in-project ([project form init] (eval/prep project) (eval/eval-in project `(do ~@(map (fn [[k v]] `(set! ~k ~v)) (:global-vars project)) ~init ~@(:injections project) ~form))) ([project form] (eval-in-project project form nil))) | ||||||||||||||||
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-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*))))) | |||||||||||||||
Compilation | ||||||||||||||||
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}] (let [all-nses (bultitude/namespaces-on-classpath :classpath (map io/file (get-classpath project))) include (case aot :all (stale-namespaces (assoc project :aot :all)) :all-with-unused all-nses aot) exclude aot-exclude-ns {include-nses false, include-regexps true} (group-by #(instance? Pattern %) include) {exclude-nses false, exclude-regexps true} (group-by #(instance? Pattern %) exclude)] (->> (set/difference (set (map str (if (seq include-regexps) all-nses include-nses))) (set exclude-nses)) (filter (fn [ns] (if (seq include-regexps) (some #(re-matches % ns) include-regexps) true))) (remove (fn [ns] (if (seq exclude-regexps) (some #(re-matches % ns) exclude-regexps) false))) (concat (if (seq include-regexps) include-nses ())) (map symbol)))) | |||||||||||||||
Compiles Clojure files into .class files. If 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 lean-compile skummet-skip-vars]} :android {:keys [nrepl-middleware]} :repl-options :as project}] (info "Compiling Clojure files...") (debug "Project classpath:" (get-classpath project)) (let [nses (namespaces-to-compile project) dev-build (dev-build? project) package-name (try (get-package-name manifest-path) (catch Exception ex nil)) 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 package-name} (not dev-build) (assoc :elide-meta [:doc :file :line :column :added :author :static :arglists :forms]))] (info (format "Build type: %s, dynamic compilation: %s, remote REPL: %s." (if dev-build "debug" (if lean-compile "lean" "release")) (if (or dev-build start-nrepl-server enable-dynamic-compilation) "enabled" "disabled") (if (or dev-build start-nrepl-server) "enabled" "disabled"))) (let [form (if lean-compile `(let [lean-var?# (fn [var#] (not (#{~@skummet-skip-vars} (str var#))))] (binding [~'clojure.core/*lean-var?* lean-var?# ~'clojure.core/*lean-compile* true ~'clojure.core/*compiler-options* ~opts] (doseq [namespace# '~nses] (println "Compiling" namespace#) (clojure.core/compile namespace#)) (shutdown-agents))) `(binding [*compiler-options* ~opts] ;; If expectations is present, don't run it during compilation. (doseq [namespace# '~nses] (println "Compiling" namespace#) (clojure.core/compile namespace#)) (try (require 'expectations) ((resolve 'expectations/disable-run-on-shutdown)) (catch Throwable _# nil)) (shutdown-agents)))] (.mkdirs (io/file (:compile-path project))) (try (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 lean-compile]} :android, java-only :java-only :as project}] (ensure-paths sdk-path) (let [project (-> project (update-in [:prep-tasks] (partial remove #{"compile"})))] (leiningen.javac/javac project) (when-not java-only (save-data-readers-to-resource project) (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 append-suffix prompt-user sdk-binary]] [reply.main :only (launch-nrepl)]) (:require [clojure.java.io :as io] [cemerick.pomegranate.aether :as aether] [reply.initialization :as reply-init])) | |||||||||||||||
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 | (defn get-device-args [adb-bin device-args] (or device-args (list "-s" (choose-device adb-bin)))) | |||||||||||||||
Messages which | (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 | (defn install [{{:keys [out-apk-path manifest-path rename-manifest-package]} :android :as project} & device-args] (info "Installing APK...") (let [adb-bin (sdk-binary project :adb) _ (ensure-paths out-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" out-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) package-name (or rename-manifest-package (get-package-name manifest-path))] (if (.equalsIgnoreCase "y" resp) (do (sh adb-bin device "uninstall" package-name) (sh adb-bin device "install" out-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, root :root :as project} & device-args] (info "Binding device port" repl-device-port "to local port" repl-local-port "...") (spit (io/file root ".nrepl-port") repl-local-port) (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)))) | |||||||||||||||
Substitution for REPLy's own | (defn default-init [{:keys [custom-help] :as options}] `(do ~@reply-init/prelude (use '[clojure.repl :only ~'[source apropos dir doc pst find-doc]]) (use '[clojure.pprint :only ~'[pp pprint]]) (defn ~'help "Prints a list of helpful commands." [] (println " Exit: Control+D or (exit) or (quit)") (println " Commands: (user/help)") (println " Docs: (doc function-name-here)") (println " (find-doc \"part-of-name-here\")") (println " Source: (source function-name-here)")) (user/help) nil)) | |||||||||||||||
Connects to a remote nREPL server on the device using REPLy. | (defn repl [{{:keys [repl-local-port]} :android}] (with-redefs [reply-init/default-init-code default-init] (launch-nrepl {:attach (str "localhost:" repl-local-port)}))) | |||||||||||||||
Metatask. Runs | (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))) | |||||||||||||||
Install the generated AAR package to the local Maven repository. | (defn local-repo [{:keys [target-path name group version root] :as project}] (leiningen.pom/pom (assoc project :packaging "aar")) (let [aar-file (io/file target-path (format "%s-%s.aar" name version))] (ensure-paths aar-file) (->> {[:extension "pom"] (io/file root "pom.xml") [:extension "aar"] aar-file} (#'aether/artifacts-for [(symbol group name) version]) (aether/install-artifacts :files)))) | |||||||||||||||
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.string :as str] [clojure.zip :refer [up xml-zip]] [clostache.parser :as clostache] [leiningen.core.main :refer [info debug abort]] [leiningen.droid.aar :refer [get-aar-files]] [leiningen.droid.utils :refer [dev-build?]] [leiningen.release :refer [parse-semantic-version]]) (:import com.android.manifmerger.ManifestMerger com.android.manifmerger.MergerLog [com.android.utils StdLogger StdLogger$Level] java.io.File)) | |||||||||||||||
Constants | ||||||||||||||||
Name of the category for the launcher activities. | (def ^{:private true} launcher-category "android.intent.category.LAUNCHER") | |||||||||||||||
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)) | |||||||||||||||
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))) | |||||||||||||||
Manifest parsing and data extraction | ||||||||||||||||
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) "/" (if (.startsWith activity-name ".") (str pkg-name activity-name) activity-name))))) | |||||||||||||||
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)] (or (first (xml-> uses-sdk (attr target-sdk-attribute))) (first (xml-> uses-sdk (attr min-sdk-attribute)))))) | |||||||||||||||
Manifest templating | ||||||||||||||||
Maximum values per each version bucket. | (def ^:private version-maximums (mapv (partial bit-shift-left 1) [9 9 9 5])) | |||||||||||||||
Each part of the version number will be multiplied by the respective coefficient, all of which are calculated here. | (def ^:private version-coefficients (->> version-maximums (reductions +) (mapv (fn [offset] (bit-shift-left 1 (- 32 offset)))))) | |||||||||||||||
Asserts that a>b in version segments | (defn- assert> [a b] (when-not (> a b) (abort (format "Version number segment too large to fit in the version-code scheme: %s > %s, maximum version in each segment is %s" b a (str/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 +))) | |||||||||||||||
Merges the main application manifest file with manifests from AAR files. | (defn merge-manifests [{{:keys [manifest-path manifest-main-app-path]} :android :as project}] (let [merger (ManifestMerger. (MergerLog/wrapSdkLog (StdLogger. StdLogger$Level/VERBOSE)) nil) lib-manifests (get-aar-files project "AndroidManifest.xml")] (debug "Merging secondary manifests:" lib-manifests) (.process merger (jio/file manifest-path) (jio/file manifest-main-app-path) (into-array File lib-manifests) nil nil))) | |||||||||||||||
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 manifest-main-app-path target-version]} :android, version :version :as project}] (info "Generating manifest...") (let [full-manifest-map (merge {:version-name version :version-code (-> version parse-semantic-version version-code) :target-version target-version :debug-build (dev-build? project)} manifest-options)] (jio/make-parents manifest-path) (->> full-manifest-map (clostache/render (slurp manifest-template-path)) (spit manifest-main-app-path)) (merge-manifests project))) | |||||||||||||||
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]])) | |||||||||||||||
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 #"\." "/")) | ||||||||||||||||
(defn package-name-valid? [package-name] (and (not (.startsWith package-name ".")) (> (.indexOf package-name ".") -1) (= (.indexOf package-name "-") -1))) | ||||||||||||||||
Creates project.clj file within an existing Android library 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) data {:name (.getName (io/file current-dir)) :target-sdk (or (get-target-sdk-version manifest-path) "15")} render (renderer "templates")] (info "Creating project.clj...") (io/copy (render "library.project.clj" data) (io/file current-dir "project.clj"))))) | |||||||||||||||
Creates new Android library. | (defn new-library [library-name package-name data] (let [render (renderer "templates")] (info "Creating library" library-name "...") (->files data "assets" [".gitignore" (render "gitignore")] ["LICENSE" (render "LICENSE")] ["README.md" (render "README.library.md" data)] ["AndroidManifest.template.xml" (render "AndroidManifest.library.xml" data)] ["project.clj" (render "library.project.clj" data)] ["res/values/strings.xml" (render "strings.library.xml" data)] ["src/java/{{path}}/Util.java" (render "Util.java" data)] ["src/clojure/{{path}}/main.clj" (render "core.clj" data)]))) | |||||||||||||||
Creates new Android application. | (defn new-application [project-name package-name data] (let [render (renderer "templates")] (info "Creating project" project-name "...") (->files data "assets" [".gitignore" (render "gitignore")] ["LICENSE" (render "LICENSE" data)] ["README.md" (render "README.md" data)] ["AndroidManifest.template.xml" (render "AndroidManifest.template.xml" data)] ["project.clj" (render "project.clj" data)] ["build/proguard-minify.cfg" (render "proguard_minify.cfg" data)] ["build/proguard-multi-dex.cfg" (render "proguard_multi_dex.cfg" 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/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)]))) | |||||||||||||||
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") :min-sdk (get options ":min-sdk" "15") :app-name (get options ":app-name" project-name) :library (get options ":library" false) :new-project true}] (if (= (:library data) "true") (new-library project-name package-name data) (new-application project-name package-name data)))) | |||||||||||||||
Functions to interact with Android SDK tools. | (ns leiningen.droid.sdk (:use [leiningen.core.main :only [debug abort]]) (:require [leiningen.droid.aar :refer [get-aar-native-paths]] [leiningen.droid.sideload :as sideload] [clojure.java.io :as io]) (:import java.io.File)) | |||||||||||||||
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 :as project} & {:keys [apk-name resource-jars]}] (sideload/sideload-jars sdk-path) (let [apkbuilder (sideload/apk-builder apk-name out-res-pkg-path out-dex-path) all-native-libraries (concat native-libraries-paths (get-unpacked-natives-paths) (get-aar-native-paths project)) 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)))) (if (seq dexes) (do (debug "Adding DEX files: " dexes) (doseq [dex dexes] (.addFile apkbuilder dex (.getName dex)))) (abort "No *.dex files found in " out-dex-path)) (.sealApk apkbuilder))) | |||||||||||||||
Wrappers around classes and methods that we pull dynamically from jars in Android SDK. | (ns leiningen.droid.sideload (:require [cemerick.pomegranate :as cp] [clojure.java.io :as io]) (:import [java.io File PrintStream])) | |||||||||||||||
Dynamically adds jars from Android SDK on the classpath. | (def sideload-jars (memoize (fn [sdk-path] (cp/add-classpath (io/file sdk-path "tools" "lib" "sdklib.jar"))))) | |||||||||||||||
Uses reflection to make an ApkBuilder instance. | (defn 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])))) | |||||||||||||||
Uses reflection to make an SymbolLoader instance. | (defn symbol-loader [file] (let [sl-class (Class/forName "com.android.sdklib.internal.build.SymbolLoader") constructor (. sl-class getConstructor (into-array [File]))] (.newInstance constructor (into-array [(io/file file)])))) | |||||||||||||||
Uses reflection to make an SymbolLoader instance. | (defn symbol-writer [out-folder package-name full-symbols] (let [sl-class (Class/forName "com.android.sdklib.internal.build.SymbolLoader") sw-class (Class/forName "com.android.sdklib.internal.build.SymbolWriter") constructor (. sw-class getConstructor (into-array [String String sl-class]))] (.newInstance constructor (into-array Object [out-folder package-name full-symbols])))) | |||||||||||||||
(ns leiningen.droid.test (:refer-clojure :exclude [test]) (:require [bultitude.core :as b] [clojure.java.io :as io] [clojure.string :as str] [clojure.set :as set] [leiningen.core.classpath :as cp] [leiningen.droid.code-gen :as code-gen] [leiningen.droid.compile :as compile] [leiningen.droid.utils :as utils])) | ||||||||||||||||
Runs tests locally using Robolectric. | (defn local-test [{{:keys [cloverage-exclude-ns]} :android :as project} & [mode]] (when-not (-> project :android :library) (code-gen/code-gen project)) (compile/compile project) (let [src-nses (b/namespaces-on-classpath :classpath (map io/file (distinct (:source-paths project))) :ignore-unreadable? false) src-nses (set/difference (set src-nses) (set (map symbol cloverage-exclude-ns))) test-nses (b/namespaces-on-classpath :classpath (map io/file (distinct (:test-paths project))) :ignore-unreadable? false) cpath (cp/get-classpath project) mode (or mode "clojuretest")] (binding [utils/*sh-print-output* true] (utils/sh "java" "-cp" (str/join ":" cpath) "coa.droid_test.internal.TestRunner" "-mode" mode ":src" (map str src-nses) ":test" (map str test-nses))))) | |||||||||||||||
Provides utilities for the plugin. | (ns leiningen.droid.utils (:require [leiningen.core.project :as pr] [robert.hooke :refer [with-hooks-disabled]]) (:use [clojure.java.io :only (file reader)] [leiningen.core.main :only (info debug abort *debug*)] [leiningen.core.classpath :only [resolve-dependencies]] [clojure.string :only (join)]) (:import [java.io File StringWriter])) | |||||||||||||||
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
| (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 path to the correct Android Build Tools directory. | (defn get-sdk-build-tools-path ([{{:keys [sdk-path build-tools-version]} :android}] (get-sdk-build-tools-path sdk-path build-tools-version)) ([sdk-path build-tools-version] (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 last 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."))] (file bt-root-dir bt-dir)))) | |||||||||||||||
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 [build-tools (get-sdk-build-tools-path sdk-path build-tools-version)] {:dx {:unix (file build-tools "dx") :win (file build-tools "dx.bat")} :adb {:unix (file sdk-path "platform-tools" "adb") :win (file sdk-path "platform-tools" "adb.exe")} :aapt {:unix (file build-tools "aapt") :win (file build-tools "aapt.exe")} :zipalign {:unix (file build-tools "zipalign") :win (file build-tools "zipalign.exe")} :proguard {:unix (file sdk-path "tools" "proguard" "bin" "proguard.sh") :win (file sdk-path "tools" "proguard" "bin" "proguard.bat")}})) | |||||||||||||||
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-str (-> (sdk-binary-paths sdk-path build-tools-version) (get-in [binary-kw (if (windows?) :win :unix)]) str)] (ensure-paths binary-str) (if (.endsWith binary-str ".bat") ["cmd.exe" "/C" binary-str] binary-str))) | |||||||||||||||
Middleware section | ||||||||||||||||
Taken from Leiningen source code. Absolutizes the | (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 | (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}] {:out-dex-path target-path :proguard-execute false :proguard-conf-path "proguard.conf" :proguard-output-jar-path (file target-path "mininified-classes.jar") :multi-dex-root-classes-path (file target-path "root-classes.jar") :multi-dex-main-dex-list-path (file target-path "main-dex-list.txt") :manifest-path (file target-path "AndroidManifest.xml") :manifest-main-app-path (file target-path "AndroidManifest.app.xml") :manifest-template-path "AndroidManifest.template.xml" :manifest-options {:app-name "@string/app_name"} :res-path "res" :gen-path (file target-path "gen") :out-res-path (file target-path "res") :assets-paths ["assets"] :assets-gen-path (file target-path "assets-gen") :out-res-pkg-path (file target-path (str name ".ap_")) :out-apk-path (file target-path (str name ".apk")) :keystore-path (file (System/getProperty "user.home") ".android" "debug.keystore") :key-alias "androiddebugkey" :repl-device-port 9999 :repl-local-port 9999 :target-version 15}) | |||||||||||||||
(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")))) | |||||||||||||||
Merges project's | (defn android-parameters [{:keys [android] :as project}] (let [android-params (merge (get-default-android-params project) android)] (-> project (vary-meta assoc-in [:profiles ::extras] {:java-source-paths [(:gen-path android-params)]}) (pr/merge-profiles [::extras]) (assoc :android android-params) absolutize-android-paths))) | |||||||||||||||
General utilities | ||||||||||||||||
(defn proj [] (read-project "sample/project.clj")) | ||||||||||||||||
If version keyword is passed (for example, | (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 | (defn get-sdk-android-jar ([{{:keys [sdk-path target-version]} :android :as project}] (get-sdk-android-jar sdk-path target-version)) ([sdk-root version] (str (file (get-sdk-platform-path sdk-root version) "android.jar")))) | |||||||||||||||
Returns a path to annotations.jar file. | (defn get-sdk-annotation-jar [sdk-root-or-project] (let [sdk-root (if (map? sdk-root-or-project) (get-in sdk-root-or-project [:android :sdk-path]) sdk-root-or-project)] (str (file sdk-root "tools" "support" "annotations.jar")))) | |||||||||||||||
Get the list of dependency libraries that has | (defn get-resource-jars [{:keys [dependencies] :as project}] (let [res-deps (filter (fn [[_ _ & {:as options}]] (:use-resources options)) (:dependencies project)) mod-proj (assoc project :dependencies res-deps)] (with-hooks-disabled resolve-dependencies (resolve-dependencies :dependencies mod-proj)))) | |||||||||||||||
If true, print the output of the shell command regardless of debug. | (def ^:dynamic *sh-print-output* false) | |||||||||||||||
Executes the subprocess specified in the binding list and applies
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))) out-stream# (StringWriter.) print-output?# (or *debug* *sh-print-output*)] ~@body (doseq [line# output#] (if print-output?# (info line#) (binding [*out* out-stream#] (println line#)))) (.waitFor ~process-name) (when-not (and (= (.exitValue ~process-name) 0) (not print-output?#)) (info (str out-stream#))) (when-not (= (.exitValue ~process-name) 0) (abort "Abort execution.")) output#))) | |||||||||||||||
Executes the command given by | (defn sh [& args] (let [str-args (for [arg (flatten args)] (if (instance? File arg) (.getCanonicalPath ^File arg) (str arg)))] (with-process [process str-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 | (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)))) | ||||||||||||||||