lein-droid/lein-droid0.0.9-SNAPSHOTPlugin for easy Clojure/Android development and deployment dependencies
| (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])
(:use [leiningen.core.project :only [merge-profiles unmerge-profiles]]
[leiningen.core.main :only [abort]]
[leiningen.help :only (subtask-help-for)]
[leiningen.droid.compile :only (compile clean-compile-dir code-gen)]
[leiningen.droid
[classpath :only [init-hooks]]
[build :only [create-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]]])) | ||||||
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)))) | ||||||
This function just prints the project map. | (defn foo [project & args] (println project)) | ||||||
Metatask. Performs all Android tasks from compilation to the deployment. | (defn doall
[{{:keys [library]} :android :as project} & device-args]
(if library
(build project)
(do (doto project
build apk)
(apply deploy project device-args)))) | ||||||
(declare execute-subtask) | |||||||
Metatask. Builds, packs and deploys the release version of the project. Can also take optional list of subtasks to execute (instead of
executing all of them) and arguments to | (defn release
[project & args]
(let [;; adb-args should be in the end of the argument list.
[subtasks adb-args] (split-with #(not (.startsWith % "-")) args)
subtasks (if (empty? subtasks)
["clean-compile-dir" "build" "apk" "deploy"]
subtasks)
release-project (-> project
(unmerge-profiles [:dev])
(merge-profiles [:release])
android-parameters)]
(doseq [task subtasks]
(execute-subtask release-project task adb-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 #'release #'help
#'gather-dependencies]}
droid
([project]
(help #'droid))
([project & [cmd & args]]
(init-hooks)
(let [;; Poor man's middleware here
project (when project (android-parameters project))]
(execute-subtask project cmd args)))) | ||||||
Executes a subtask defined by | (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)
"clean-compile-dir" (clean-compile-dir 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)
"gather-dependencies" (apply gather-dependencies project args)
;; 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
"foo" (foo project)
"help" (help #'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]]]
[leiningen.droid
[compile :only [code-gen compile]]
[utils :only [get-sdk-android-jar first-matched proj sh dev-build?
ensure-paths with-process read-password append-suffix
create-debug-keystore get-project-file read-project]]
[manifest :only [write-manifest-with-internet-permission]]])
(:require [clojure.java.io :as io]
leiningen.jar leiningen.javac)) | ||||||
Build-related subtasks | |||||||
Creates a DEX file from the compiled .class files. Since the execution of | (defn create-dex
[{{:keys [sdk-path out-dex-path external-classes-paths]} :android,
compile-path :compile-path :as project}]
(info "Creating DEX....")
(ensure-paths sdk-path)
(let [dx-bin (str sdk-path "/platform-tools/dx")
no-optimize (if (dev-build? project) "--no-optimize" [])
annotations (str sdk-path "/tools/support/annotations.jar")
deps (resolve-dependencies :dependencies project)
external-paths (or external-classes-paths [])]
(with-process [proc (map str
(flatten [dx-bin "--dex" no-optimize
"--output" out-dex-path
compile-path annotations deps
external-paths]))]
(.addShutdownHook (Runtime/getRuntime) (Thread. #(.destroy proc)))))) | ||||||
Updates the pre-processed PNG cache. Calls | (defn crunch-resources
[{{:keys [sdk-path res-path out-res-path]} :android}]
(info "Crunching resources...")
(ensure-paths sdk-path res-path)
(let [aapt-bin (str sdk-path "/platform-tools/aapt")]
(sh aapt-bin "crunch -v"
"-S" res-path
"-C" out-res-path))) | ||||||
We have to declare a future reference here because | (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 | (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-path res-path
out-res-path external-res-paths out-res-pkg-path]} :android
:as project}]
(info "Packaging resources...")
(ensure-paths sdk-path manifest-path res-path)
(let [aapt-bin (str sdk-path "/platform-tools/aapt")
android-jar (get-sdk-android-jar sdk-path target-version)
dev-build (dev-build? project)
manifest-file (io/file manifest-path)
backup-file (io/file (str manifest-path ".backup"))
;; Only add `assets` directory if it is present.
assets (if (.exists (io/file assets-path)) ["-A" assets-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")
(when dev-build
(io/copy backup-file manifest-file)
(io/delete-file backup-file)))) | ||||||
Creates a deployment-ready APK file. It is done by running | (defn create-apk
[{{:keys [sdk-path out-apk-path out-res-pkg-path out-dex-path]} :android,
source-paths :source-paths, java-source-paths :java-source-paths
:as project}]
(info "Creating APK...")
(ensure-paths sdk-path out-res-pkg-path out-dex-path)
(let [apkbuilder-bin (str sdk-path "/tools/apkbuilder")
suffix (if (dev-build? project) "debug-analigned" "unaligned")
unaligned-path (append-suffix out-apk-path suffix)
clojure-jar (first-matched #(re-find #"android/clojure" (str %))
(resolve-dependencies :dependencies
project))]
(sh apkbuilder-bin unaligned-path "-u"
"-z" out-res-pkg-path
"-f" out-dex-path
"-rj" (str clojure-jar)))) | ||||||
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 keystore-path key-alias]} :android :as project}]
(info "Signing APK...")
(let [dev-build (dev-build? project)
suffix (if dev-build "debug-analigned" "unaligned")
unaligned-path (append-suffix out-apk-path suffix)
storepass (if dev-build "android"
(read-password "Enter storepass: "))
keypass (if dev-build "android"
(read-password "Enter keypass: "))]
(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)
(sh "jarsigner"
"-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 (str sdk-path "/tools/zipalign")
unaligned-suffix (if (dev-build? project) "debug-analigned" "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 sdk-path 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]])
(: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 (map
(fn [[artifact version :as dep]]
(let [[_ group name] (re-find #"(.+/)?(.+)" (str artifact))]
{:name name, :group group, :ver version, :original dep}))
dependencies)
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 %) "android/")
same-jars)]
(if-not (empty? droid-clojures)
droid-clojures
same-jars))
same-jars)]
(:original
(reduce #(if (pos? (compare (.parseVersion scheme (:version %2))
(.parseVersion scheme (:version %1))))
%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))) | ||||||
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 [sdk-path target-version external-classes-paths use-google-api]}
: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-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]]])) | ||||||
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 dep)))))) | ||||||
This part of the plugin is responsible for the project compilation. | (ns leiningen.droid.compile
(:refer-clojure :exclude [compile])
(:require [leiningen compile javac clean]
[clojure.java.io :as io]
[clojure.set :as sets]
[leiningen.core.eval :as eval])
(:use [leiningen.droid.utils :only [get-sdk-android-jar
ensure-paths sh dev-build?]]
[leiningen.droid.manifest :only [get-package-name]]
[leiningen.core
[main :only [debug info abort]]
[classpath :only [get-classpath]]]
[bultitude.core :only [namespaces-on-classpath]])) | ||||||
Pre-compilation tasks | |||||||
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 code-gen
[{{:keys [sdk-path target-version manifest-path res-path gen-path
out-res-path external-res-paths library]} :android}]
(info "Generating R.java...")
(let [aapt-bin (str sdk-path "/platform-tools/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 sdk-path manifest-path res-path aapt-bin 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"))) | ||||||
Deletes all files in the project directory where files are compiled to. Used by | (defn clean-compile-dir
[{:keys [compile-path]} & _]
(leiningen.clean/delete-file-recursively compile-path :silently)) | ||||||
Compilation | |||||||
Stores a set of namespaces that should always be compiled
regardless of the build type. Since these namespaces are used in
| (def ^:private always-compile-ns
(set '(clojure.core clojure.core.protocols clojure.string
clojure.java.io neko.init.options))) | ||||||
Takes project and returns a set of namespaces that should be AOT-compiled. | (defn namespaces-to-compile
[{:keys [aot aot-exclude-ns] :as project}]
(-> (case aot
:all
(seq (leiningen.compile/stale-namespaces project))
: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 aot-exclude-ns))) | ||||||
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]} :android :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)
compiler-options (if dev-build {} {:elide-meta [:doc :file :line :added
:arglists :private]})]
(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 [o/*release-build* ~(not dev-build)
o/*start-nrepl-server* ~start-nrepl-server
o/*enable-dynamic-compilation* ~enable-dynamic-compilation
o/*package-name* ~(get-package-name manifest-path)
*compiler-options* ~compiler-options]
(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
'(require '[neko.init.options :as o]))
(info "Compilation succeeded.")
(catch Exception e
(abort "Compilation failed.")))))) | ||||||
Compiles both Java and Clojure source files. | (defn compile
[{{:keys [sdk-path]} :android, java-only :java-only :as project} & args]
(ensure-paths sdk-path)
(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)]
[leiningen.droid.manifest :only (get-launcher-activity
get-package-name)]
[leiningen.droid.utils :only (sh ensure-paths dev-build? append-suffix)]
[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\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)
(let [answer (dec (Integer/parseInt (read-line)))]
(:serial (nth devices answer))))))) | ||||||
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)))) | ||||||
Installs the APK on the only (or specified) device or emulator. | (defn install
[{{:keys [adb-bin out-apk-path manifest-path]} :android :as project}
& device-args]
(info "Installing APK...")
(ensure-paths adb-bin)
(let [apk-path (if (dev-build? project)
(append-suffix out-apk-path "debug")
out-apk-path)
device (get-device-args adb-bin device-args)]
(ensure-paths apk-path)
;; Uninstall old APK first.
(sh adb-bin device "uninstall" (get-package-name manifest-path))
(sh adb-bin device "install" "-r" apk-path))) | ||||||
Launches the installed APK on the connected device. | (defn run
[{{:keys [adb-bin manifest-path]} :android} & device-args]
(info "Launching APK...")
(ensure-paths adb-bin manifest-path)
(let [device (get-device-args adb-bin device-args)]
(sh adb-bin device "shell" "am" "start" "-n"
(get-launcher-activity manifest-path)))) | ||||||
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 [adb-bin repl-device-port repl-local-port]} :android} & device-args]
(info "Binding device port" repl-device-port
"to local port" repl-local-port "...")
(ensure-paths adb-bin)
(let [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 | (defn deploy
[{{:keys [adb-bin]} :android :as project} & device-args]
(let [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.xml :as xml])
(:use [clojure.zip :only (xml-zip up node append-child)]
[clojure.data.zip.xml])
(: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
[manifest-path]
(let [manifest (load-manifest manifest-path)
[activity-name] (-> manifest
get-all-launcher-activities
first
up up
(xml-> (attr :android:name)))]
(let [[_ pkg-name simple-name] (re-matches #"(.*\.)?(.+)" activity-name)
pkg-name (if (and pkg-name (> (count pkg-name) 1))
pkg-name
(first (xml-> manifest (attr :package))))]
(str pkg-name "/." simple-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)))) | ||||||
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 [path (string/join "/" [name (sanitize template)])]
(if data
(render-text (slurp-resource path) data)
(io/input-stream (io/resource path)))))) | ||||||
(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)))) | ||||||
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 & {:keys [activity target-sdk app-name],
:or {activity "MainActivity", target-sdk "10",
app-name project-name}}]
(let [data {:name project-name
:package package-name
:package-sanitized (sanitize package-name)
:path (package-to-path (sanitize package-name))
:activity activity
:target-sdk target-sdk
:app-name app-name}
render (renderer "templates")]
(->files
data
"assets"
["AndroidManifest.xml" (render "AndroidManifest.xml" data)]
["project.clj" (render "project.clj" data)]
["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/values/strings.xml" (render "strings.xml" data)]
"src/java"
["src/clojure/{{path}}/main.clj" (render "main.clj" data)]))) | ||||||
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)]
[clojure.string :only (join)])) | ||||||
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
[{{sdk-path :sdk-path} :android, name :name, target-path :target-path}]
{:out-dex-path (str target-path "/classes.dex")
:manifest-path "AndroidManifest.xml"
:res-path "res"
:gen-path "gen"
:out-res-path (str target-path "/res")
:assets-path "assets"
:out-res-pkg-path (str target-path "/" name ".ap_")
:out-apk-path (str target-path "/" name ".apk")
:keystore-path (str (System/getenv "HOME") "/.android/debug.keystore")
:adb-bin (str sdk-path "/platform-tools/adb")
: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 | (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 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 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")) | |||||||
Returns a version-specific path to the Android platform tools. | (defn get-sdk-platform-path [sdk-root version] (format "%s/platforms/android-%s" sdk-root version)) | ||||||
Returns a version-specific path to the | (defn get-sdk-android-jar [sdk-root version] (str (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] (format "%s/add-ons/addon-google_apis-google-%s" sdk-root version)) | ||||||
Returns a version-specific paths to all Google SDK jars. | (defn get-sdk-google-api-jars
[sdk-root version]
(map #(.getAbsolutePath %)
(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 the first item from the collection predicate | (defn first-matched [pred coll] (some (fn [item] (when (pred item) item)) coll)) | ||||||
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)))]
~@body
(.waitFor ~process-name)
(if-not (= (.exitValue ~process-name) 0)
(apply abort output#)
(apply debug output#))
output#))) | ||||||
Executes the command given by | (defn sh [& args] (with-process [process (flatten args)])) | ||||||
Checks if the current Leiningen run contains :dev profile. | (defn dev-build? [project] (contains? (-> project meta :included-profiles set) :dev)) | ||||||
Checks if the given directories or files exist. Aborts Leiningen execution in case either of them doesn't or the value equals nil. | (defmacro ensure-paths
[& paths]
`(do
~@(for [p paths]
`(cond (nil? ~p)
(abort "The value of" (str '~p) "is nil. Abort execution.")
(not (.exists (file ~p)))
(abort "The path" ~p "doesn't exist. Abort execution."))))) | ||||||
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 the password from the console without echoing the characters. | (defn read-password [prompt] (join (.readPassword (System/console) prompt nil))) | ||||||
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"
"-keyalg" "RSA"
"-keysize" "1024"
"-validity" "365"
"-keypass" "android"
"-storepass" "android"
"-dname" "CN=Android Debug,O=Android,C=US")) | ||||||