Signing
| Artifact | Signed with |
|---|---|
Declares signing | The keystore it names. |
A debug build (release unset, the default, or false), no signing | The Android debug keystore, created on first use. |
A release build (release = true, or unset under --release), no signing | Nothing: published unsigned, with a warning. |
An APK is signed with apksigner, with the signature schemes a device checks on install. A bundle is signed with the
JDK’s jarsigner, which is how a store’s upload key is applied. An unsigned APK cannot be installed, so it has no
run, debug or test target.
The debug keystore
A debug build with no signing is signed with debug.keystore in Android’s user directory (ANDROID_USER_HOME,
else ~/.android), the keystore Android Studio and the Android Gradle Plugin sign debug builds with, so an app
built by either installs over the other. When it is missing it is created with the Android debug conventions:
alias androiddebugkey, password android, CN=Android Debug,O=Android,C=US, RSA 2048, valid for 30 years. An
existing file is never replaced.
Your own keystore
artifacts {
["app"] = new Android.AndroidApk {
signing {
keystore = "../keys/release.jks"
alias = "release"
dname = "CN=Example, O=Example, C=US"
validity = 9125
passwordEnv = "RELEASE_KEYSTORE_PASS"
keyPasswordEnv = "RELEASE_KEY_PASS"
}
}
}
| Property | Default | Meaning |
|---|---|---|
keystore | required | Keystore path, relative to the project. |
alias | androiddebugkey | Key alias. |
passwordEnv | ANDROID_KEYSTORE_PASS | Variable holding the keystore password. |
keyPasswordEnv | none | Variable holding the key password, when it differs. |
dname | CN= | Distinguished name of a key Elide creates. |
validity | 10000 | Days a key Elide creates stays valid. |
Passwords never appear in the manifest or on a command line. The variables are read at signing time and handed to
apksigner, jarsigner and keytool through their environment, so the process table never shows them. An unset
variable falls back to android, the debug keystore’s password. Password values are redacted from errors.
A build that signs checks the keystore before compiling anything:
error: Keystore /path/to/app/../keys/release.jks not found
hint: Create one with: elide build android-keystore-app
Builds that do not sign (compiling, host tests) never need the keystore.
jarsigner and keytool come from a JDK: the build’s own, the one running Elide, then JAVA_HOME, then PATH.
Create a release keystore
elide build android-keystore-<artifact> creates the keystore an artifact’s signing names, once; it never
overwrites one or adds a key to it. With --release it refuses anything that would make it a debug key: an
artifact with no signing, the debug keystore’s path, the alias androiddebugkey, the debug distinguished name,
the default validity, and a passwordEnv that is unset.
export RELEASE_KEYSTORE_PASS=… # from your secret store
export RELEASE_KEY_PASS=…
elide build android-keystore-app --release
Keep the keystore out of the repository, and back it up: an app on a store can only be updated with the key it was first signed with (or, with Play App Signing, the upload key Play knows).
Release builds
elide build --release
An application that leaves release unset is a debug build: debuggable, and signed with the debug keystore when it
declares no signing. --release makes every such application a release build; one that declares release is what
it declares, with or without the flag. --release also decides minify for an application that leaves it unset:
| The application | Without --release | With --release |
|---|---|---|
declares minify | as declared | as declared |
declares proguardFiles, or consumes a workspace library declaring consumerProguardFiles | d8 | R8 |
| consumes published libraries only | d8 | R8 if one ships proguard.txt, else d8 |
| is a bundle declaring feature modules | d8 | d8 |
In the third case the build says so when it falls back to d8 (… with d8, unshrunk: neither 'app' nor its libraries declare ProGuard rules). Under --release, an application that may be shrunk has no instrumented tests
(Testing). What a build wrote without --release is never reported up to date for a build with
it.
| Setting | Decides |
|---|---|
release | Whether debug information is kept and the app is debuggable. |
minify | Whether R8 shrinks, optimizes and obfuscates the code, in place of d8. |
Either can be set without the other. A release APK for a store:
artifacts {
["app"] = new Android.AndroidApk {
versionCode = 4
versionName = "1.3.0"
release = true
minify = true
proguardFiles { "proguard-rules.pro" }
signing {
keystore = "../keys/release.jks"
alias = "release"
dname = "CN=Example, O=Example, C=US"
validity = 9125
passwordEnv = "RELEASE_KEYSTORE_PASS"
}
}
}
Shrinking with R8
R8 is pinned per Elide release, downloaded once and verified by SHA-256; the Android SDK does not ship it. Its rules
are applied in one chain: what consumed libraries contribute (a published AAR’s proguard.txt, a workspace
library’s consumerProguardFiles), then the artifact’s own proguardFiles. A build that would run R8 with no
rules fails instead, since R8 with nothing to keep strips the app to nothing.
Ahead of that chain, R8 is given the keep rules aapt2 link --proguard writes for the classes the merged manifest
and the resources name: the application class, activities, services, receivers, providers, instrumentation, and views
a layout inflates. The platform instantiates these by name, so a shrink never removes them, and your rules need keep
only what the app reaches by reflection. A declared minify whose libraries and proguardFiles contribute no rule
is still refused.
# proguard-rules.pro
# Keep line numbers, so stack traces from the field can be mapped back.
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
mapping.txt
A minified build publishes R8’s mapping beside the archive, as .dev/artifacts/<artifact>-mapping.txt. It is the
only way to read a stack trace from a shipped build: keep it with every release, and upload it to the store with
the bundle. A shrink served from the build cache still publishes it.
The lockfile records which d8 or R8 produced each application’s dex (r8 9.4.17, or d8 build-tools;35.0.0),
and elide project info shows it (Android SDK).
App bundles
Stores take an app bundle rather than an APK:
export UPLOAD_KEYSTORE_PASS=…
elide build android-keystore-bundle --release # once
elide build --release bundle
The bundle (.dev/artifacts/bundle.aab) is signed with the upload key; the store re-signs the APKs it generates with
the app signing key. A bundle with dynamic feature modules cannot be minified. Bundle settings are covered in
Manifest and resources.
Reproducible builds
A build whose SDK components, R8 and bundletool releases are all recorded in the lockfile produces the same unsigned,
aligned APK and the same unsigned bundle on another machine, from another checkout path and in another time zone.
Every archive Elide writes stamps its entries with a fixed date and adds them in a fixed order, tool inputs are
sorted, and no host path reaches an archive. Signing is outside this: a JAR signature records when it was made, so
compare the aligned APK (.dev/android/<a>/aligned.apk) or an unsigned archive. Commit .dev/elide.lock.v2.bin, run elide install before building in CI, and re-resolve
deliberately with elide install --update (Android SDK).
elide install --accept-android-licenses
elide build --release