# Signing and release

> Sign Android apps with a debug or release keystore, keep passwords in the environment, and build shrunk, signed APKs and bundles with elide build --release.

## 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

```pkl
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=Android Debug, O=Android, C=US` | 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:

```text
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.

```bash
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

```bash
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](https://elide.help/docs/toolchain/android/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:

```pkl
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.

```text
# 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](https://elide.help/docs/toolchain/android/sdk#lockfile)).

### App bundles

Stores take an app bundle rather than an APK:

```bash
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](https://elide.help/docs/toolchain/android/manifest#app-bundles).

## 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](https://elide.help/docs/toolchain/android/sdk#lockfile)).

```bash
elide install --accept-android-licenses
elide build --release
```
