feat(android): sign release builds with the persistent keystore
build_android now requires the keystore generated by the previous commit, prompts for its passwords at build time, and signs the APK with it instead of an ephemeral debug key auto-generated per container run — that was the actual cause of every release needing a manual uninstall to update. Also derives versionCode from <Version> instead of leaving it fixed at 1, so version ordering stays monotonic across releases. Docs (docker/README.md, CLAUDE.md) updated to match the new signing flow.
This commit is contained in:
@@ -47,7 +47,7 @@ by `MainWindow` on desktop and as the single-view root on Android.
|
|||||||
- Tests (headless, the primary verification layer): `dotnet test` — single: `dotnet test --filter "FullyQualifiedName~TestName"`; property-based tests (CsCheck, `PropertyTests.cs`) run in the same command and take ~30 s
|
- Tests (headless, the primary verification layer): `dotnet test` — single: `dotnet test --filter "FullyQualifiedName~TestName"`; property-based tests (CsCheck, `PropertyTests.cs`) run in the same command and take ~30 s
|
||||||
- GUI hot reload: `dotnet watch --project src/App.Desktop` (on WSL2/WSLg the window shows on the Windows desktop, no graphics dependencies to install)
|
- GUI hot reload: `dotnet watch --project src/App.Desktop` (on WSL2/WSLg the window shows on the Windows desktop, no graphics dependencies to install)
|
||||||
- CLI: `dotnet run --project src/Cli -- <command>` (no args → usage)
|
- CLI: `dotnet run --project src/Cli -- <command>` (no args → usage)
|
||||||
- **Release binaries (all 3 targets): `./docker/build.sh [windows|linux|android|all]`** — reproducible builds in Docker (toolchain pinned in `docker/Dockerfile.*`, no SDK needed on host), artifacts in `dist/`, version taken from the App csproj. See `docker/README.md`. Gotchas already encoded there: single-file desktop publishes need `-p:IncludeNativeLibrariesForSelfExtract=true` (without it Avalonia's native libs — Skia/HarfBuzz/ANGLE — stay outside the exe, which then silently fails to start); the android workload dictates the SDK API level (error XA5207 → bump `ANDROID_SDK_PLATFORM` in `Dockerfile.android`).
|
- **Release binaries (all 3 targets): `./docker/build.sh [windows|linux|android|all]`** — reproducible builds in Docker (toolchain pinned in `docker/Dockerfile.*`, no SDK needed on host), artifacts in `dist/`, version taken from the App csproj. See `docker/README.md`. Gotchas already encoded there: single-file desktop publishes need `-p:IncludeNativeLibrariesForSelfExtract=true` (without it Avalonia's native libs — Skia/HarfBuzz/ANGLE — stay outside the exe, which then silently fails to start); the android workload dictates the SDK API level (error XA5207 → bump `ANDROID_SDK_PLATFORM` in `Dockerfile.android`). Android release builds need a persistent signing keystore, generated once with `docker/keystore/generate-keystore.sh` (never committed — see `docker/keystore/README.md`): without it every build gets a different random signature and users must uninstall the old app to receive an update instead of updating in place.
|
||||||
- Manual Windows publish: `dotnet publish src/App.Desktop -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --self-contained`
|
- Manual Windows publish: `dotnet publish src/App.Desktop -r win-x64 -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true --self-contained`
|
||||||
- Manual Linux publish: same with `-r linux-x64` (single-file binary; AppImage via PupNet Deploy is a future step, no pupnet.conf yet)
|
- Manual Linux publish: same with `-r linux-x64` (single-file binary; AppImage via PupNet Deploy is a future step, no pupnet.conf yet)
|
||||||
|
|
||||||
|
|||||||
+25
-9
@@ -46,6 +46,16 @@ automatically on first use — a few minutes for desktop, 10–20 minutes for
|
|||||||
Android (large downloads). Subsequent builds reuse the cached images and take
|
Android (large downloads). Subsequent builds reuse the cached images and take
|
||||||
well under a minute (desktop) / a few minutes (Android).
|
well under a minute (desktop) / a few minutes (Android).
|
||||||
|
|
||||||
|
**Android release signing:** before the first `android` build, generate the
|
||||||
|
persistent signing keystore once — see [`keystore/README.md`](keystore/README.md):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./docker/keystore/generate-keystore.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Without it, `build_android` refuses to run — every APK must be signed with
|
||||||
|
the same key so future releases can update a previous install in place.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
@@ -64,7 +74,7 @@ Running without arguments shows an interactive menu — pick a single target or
|
|||||||
Targets:
|
Targets:
|
||||||
windows Win x64 single-file executable (native libs embedded)
|
windows Win x64 single-file executable (native libs embedded)
|
||||||
linux Linux x64 single-file binary (runs as-is, nothing to install)
|
linux Linux x64 single-file binary (runs as-is, nothing to install)
|
||||||
android Android APK (debug-signed)
|
android Android APK (release-signed, prompts for keystore passwords)
|
||||||
all All three targets
|
all All three targets
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
@@ -109,16 +119,16 @@ effectively all of them); no .NET or other packages to install. If you
|
|||||||
transfer it through a channel that strips permissions (e.g. a web download),
|
transfer it through a channel that strips permissions (e.g. a web download),
|
||||||
restore the execute bit with `chmod +x`.
|
restore the execute bit with `chmod +x`.
|
||||||
|
|
||||||
**Android** — a debug-signed APK for sideloading: transfer it to the phone
|
**Android** — a release-signed APK for sideloading: transfer it to the phone
|
||||||
and open it (enable "install from unknown sources" if prompted), or install
|
and open it (enable "install from unknown sources" if prompted), or install
|
||||||
via `adb install dist/android/PalladiumWallet-*.apk`. Supports Android 6.0+
|
via `adb install dist/android/PalladiumWallet-*.apk`. Supports Android 6.0+
|
||||||
(API 23), arm64 phones and x86_64 emulators.
|
(API 23), arm64 phones and x86_64 emulators.
|
||||||
|
|
||||||
> **Signature caveat:** debug-signed APKs built on different machines carry
|
> **Signature:** every APK is signed with the persistent keystore in
|
||||||
> different keys. If a previous build is already installed, Android refuses
|
> `docker/keystore/` (see [Prerequisites](#prerequisites)), so installing a
|
||||||
> the update (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) — uninstall the old app
|
> newer build over an existing one updates it in place — no uninstall, no
|
||||||
> first. **Uninstalling deletes the app's data: back up the wallet seed
|
> data loss. This only holds as long as every release keeps using that same
|
||||||
> before doing this.** Release signing with a stable key is not set up yet.
|
> keystore file; see `docker/keystore/README.md` for the backup story.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -166,8 +176,14 @@ docker volume rm plm-nuget-cache
|
|||||||
android workload moved to a newer API level. Bump
|
android workload moved to a newer API level. Bump
|
||||||
`ANDROID_SDK_PLATFORM` (and `ANDROID_SDK_BUILD_TOOLS`) in
|
`ANDROID_SDK_PLATFORM` (and `ANDROID_SDK_BUILD_TOOLS`) in
|
||||||
`Dockerfile.android` to the level the error names, then `--rebuild`.
|
`Dockerfile.android` to the level the error names, then `--rebuild`.
|
||||||
- **APK won't install over an existing app** — signature mismatch between
|
- **APK won't install over an existing app
|
||||||
debug keys; see the signature caveat above.
|
(`INSTALL_FAILED_UPDATE_INCOMPATIBLE`)** — the new build wasn't signed with
|
||||||
|
the same keystore as the installed one. Make sure `docker/keystore/release.keystore`
|
||||||
|
hasn't changed since the installed build; if it's genuinely a different key,
|
||||||
|
the user must uninstall the old app first (this deletes app data — back up
|
||||||
|
the wallet seed before doing this).
|
||||||
|
- **`build_android` refuses to run, asks to generate a keystore** — run
|
||||||
|
`./docker/keystore/generate-keystore.sh` once (see `docker/keystore/README.md`).
|
||||||
- **Everything is broken / start from scratch** —
|
- **Everything is broken / start from scratch** —
|
||||||
`docker system prune -a && docker volume rm plm-nuget-cache`, then rerun
|
`docker system prune -a && docker volume rm plm-nuget-cache`, then rerun
|
||||||
the script (images and packages are re-downloaded).
|
the script (images and packages are re-downloaded).
|
||||||
|
|||||||
+36
-2
@@ -29,7 +29,7 @@ $(bold "Usage:") $(basename "$0") [TARGET] [OPTIONS]
|
|||||||
$(bold "Targets:")
|
$(bold "Targets:")
|
||||||
windows Win x64 single-file executable → dist/windows/
|
windows Win x64 single-file executable → dist/windows/
|
||||||
linux Linux x64 single-file binary → dist/linux/
|
linux Linux x64 single-file binary → dist/linux/
|
||||||
android Android APK (debug-signed) → dist/android/
|
android Android APK (release-signed) → dist/android/
|
||||||
all All three targets
|
all All three targets
|
||||||
|
|
||||||
$(bold "Options:")
|
$(bold "Options:")
|
||||||
@@ -112,6 +112,10 @@ ensure_android_image() {
|
|||||||
# $1 = image name, $2 = inline bash commands to execute inside the container.
|
# $1 = image name, $2 = inline bash commands to execute inside the container.
|
||||||
# Source is copied to /tmp/build inside the container so bin/obj never pollute
|
# Source is copied to /tmp/build inside the container so bin/obj never pollute
|
||||||
# the repo. Output is written to /output (→ host DIST_DIR sub-folder).
|
# the repo. Output is written to /output (→ host DIST_DIR sub-folder).
|
||||||
|
# Optional extra `docker run` args (e.g. keystore mount, signing passwords via
|
||||||
|
# -e) can be set by the caller in the EXTRA_DOCKER_ARGS array beforehand.
|
||||||
|
EXTRA_DOCKER_ARGS=()
|
||||||
|
|
||||||
run_build() {
|
run_build() {
|
||||||
local image="$1"
|
local image="$1"
|
||||||
local commands="$2"
|
local commands="$2"
|
||||||
@@ -123,6 +127,7 @@ run_build() {
|
|||||||
--volume "${PROJECT_ROOT}:/src:ro" \
|
--volume "${PROJECT_ROOT}:/src:ro" \
|
||||||
--volume "${out_dir}:/output" \
|
--volume "${out_dir}:/output" \
|
||||||
--volume "${NUGET_VOLUME}:/root/.nuget/packages" \
|
--volume "${NUGET_VOLUME}:/root/.nuget/packages" \
|
||||||
|
"${EXTRA_DOCKER_ARGS[@]}" \
|
||||||
"$image" \
|
"$image" \
|
||||||
bash -euo pipefail -c "
|
bash -euo pipefail -c "
|
||||||
cp -r /src/. /tmp/build
|
cp -r /src/. /tmp/build
|
||||||
@@ -169,16 +174,45 @@ build_linux() {
|
|||||||
|
|
||||||
build_android() {
|
build_android() {
|
||||||
ensure_android_image
|
ensure_android_image
|
||||||
|
|
||||||
|
local keystore="${SCRIPT_DIR}/keystore/release.keystore"
|
||||||
|
[[ -f "$keystore" ]] || die "No release keystore found at docker/keystore/release.keystore. Run ./docker/keystore/generate-keystore.sh once first (see docker/keystore/README.md)."
|
||||||
|
|
||||||
info "Building Android APK …"
|
info "Building Android APK …"
|
||||||
|
read -r -s -p "Keystore password: " ANDROID_KS_PASS
|
||||||
|
echo
|
||||||
|
read -r -s -p "Key password (press Enter to reuse the keystore password): " ANDROID_KEY_PASS
|
||||||
|
echo
|
||||||
|
ANDROID_KEY_PASS="${ANDROID_KEY_PASS:-$ANDROID_KS_PASS}"
|
||||||
|
|
||||||
|
# versionCode derived from <Version> (MAJOR.MINOR.PATCH, pre-release suffix
|
||||||
|
# stripped) so it always increases with releases — required for some
|
||||||
|
# installers to accept an in-place update even when the signature matches.
|
||||||
|
local semver="${VERSION%%-*}"
|
||||||
|
IFS='.' read -r VMAJOR VMINOR VPATCH <<< "$semver"
|
||||||
|
local VERSION_CODE=$(( ${VMAJOR:-0} * 10000 + ${VMINOR:-0} * 100 + ${VPATCH:-0} ))
|
||||||
|
info "versionCode: ${VERSION_CODE}"
|
||||||
|
|
||||||
|
EXTRA_DOCKER_ARGS=(
|
||||||
|
--volume "${keystore}:/keystore/release.keystore:ro"
|
||||||
|
--env "ANDROID_KS_PASS=${ANDROID_KS_PASS}"
|
||||||
|
--env "ANDROID_KEY_PASS=${ANDROID_KEY_PASS}"
|
||||||
|
)
|
||||||
run_build "$IMAGE_ANDROID" \
|
run_build "$IMAGE_ANDROID" \
|
||||||
"JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
"JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
|
||||||
dotnet build src/App.Android \
|
dotnet build src/App.Android \
|
||||||
-c Release \
|
-c Release \
|
||||||
-t:SignAndroidPackage \
|
-t:SignAndroidPackage \
|
||||||
-p:AndroidSdkDirectory=\${ANDROID_HOME}
|
-p:AndroidSdkDirectory=\${ANDROID_HOME} \
|
||||||
|
-p:ApplicationVersion=${VERSION_CODE} \
|
||||||
|
-p:AndroidSigningKeyStore=/keystore/release.keystore \
|
||||||
|
-p:AndroidSigningKeyAlias=palladiumwallet \
|
||||||
|
-p:AndroidSigningStorePass=\${ANDROID_KS_PASS} \
|
||||||
|
-p:AndroidSigningKeyPass=\${ANDROID_KEY_PASS}
|
||||||
cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \
|
cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \
|
||||||
\"/output/PalladiumWallet-${VERSION}.apk\"" \
|
\"/output/PalladiumWallet-${VERSION}.apk\"" \
|
||||||
"${DIST_DIR}/android"
|
"${DIST_DIR}/android"
|
||||||
|
EXTRA_DOCKER_ARGS=()
|
||||||
ok "Android → dist/android/PalladiumWallet-${VERSION}.apk"
|
ok "Android → dist/android/PalladiumWallet-${VERSION}.apk"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user