Krolyxon
Commit 606eee1

Branches

Refs →

budctl606eee1

Releases carry the Android app: budctl.apk, signed with budctl's key

Step 6 of docs/android.md.

- Gradle signs release builds with the owner's key when BUDCTL_KEYSTORE
  and BUDCTL_KEYSTORE_PASSWORD are set, and leaves them unsigned
  otherwise. The key (RSA 4096, alias budctl) lives outside the repo,
  in ~/.local/share/budctl-release on the owner's machine and in the
  repository's secrets BUDCTL_KEYSTORE_BASE64 and
  BUDCTL_KEYSTORE_PASSWORD.
- The release workflow builds and tests the app, checks the APK's
  signature with apksigner, and publishes budctl.apk and its .sha256
  with the Linux tarballs.
- CI builds the app (debug, phone and emulator) and runs its JVM tests
  on every push, the manifest's no-INTERNET check with them.
- The README says how to install the APK, gives the certificate's
  SHA-256 fingerprint, and says that a build of one's own and a release
  cannot replace each other.

New in CI: actions/setup-java (a JDK for Gradle) and cargo-ndk, built
there with cargo install, as on the owner's machine.
Author
krolyxon
Date
2026-11-01 23:35 +0530
Parent
090b358
Commit
606eee13a6837e7c9418a0c47209bfa880ad7b18

+105 −12

6 files changed

  1. M .github/workflows/ci.yml +19 −0
  2. M .github/workflows/release.yml +44 −4
  3. M CLAUDE.md +3 −2
  4. M README.md +12 −2
  5. M android/app/build.gradle.kts +16 −0
  6. M docs/android.md +11 −4
M .github/workflows/ci.yml View
@@ -23,3 +23,22 @@ jobs:
2323 - run: cargo clippy -p budctl-core --target aarch64-linux-android --locked -- -D warnings
2424 - run: cargo test --workspace --locked
2525 - run: sh -n install.sh
26+
27+ # The Android app: its Rust library (debug, for the phone and the emulator), the app and its JVM
28+ # tests. The manifest check (no INTERNET) runs with the build.
29+ android:
30+ runs-on: ubuntu-24.04
31+ steps:
32+ - uses: actions/checkout@v6
33+ - uses: actions/setup-java@v6
34+ with:
35+ distribution: temurin
36+ java-version: 21
37+ - uses: dtolnay/rust-toolchain@stable
38+ with:
39+ targets: aarch64-linux-android, x86_64-linux-android
40+ - run: cargo install cargo-ndk --locked
41+ - name: Android SDK packages
42+ run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;30.0.16248370" "platforms;android-37.0" "build-tools;36.0.0" > /dev/null
43+ - run: ./gradlew --no-daemon assembleDebug testDebugUnitTest
44+ working-directory: android
M .github/workflows/release.yml View
@@ -1,8 +1,10 @@
11 name: Release
22
3−# A tag like v0.1.1 builds budctl and budctl-gui for x86_64 and aarch64 Linux and publishes them,
4−# with install.sh and checksums, as a GitHub release. The builds run on Ubuntu 22.04, so they need
5−# glibc 2.35 or newer and nothing else at build time: Wayland and xkbcommon are loaded at runtime.
3+# A tag like v0.1.1 builds budctl and budctl-gui for x86_64 and aarch64 Linux and the Android app,
4+# and publishes them, with install.sh and checksums, as a GitHub release. The Linux builds run on
5+# Ubuntu 22.04, so they need glibc 2.35 or newer and nothing else at build time: Wayland and
6+# xkbcommon are loaded at runtime. The APK is signed with the owner's key, from the repository's
7+# secrets BUDCTL_KEYSTORE_BASE64 and BUDCTL_KEYSTORE_PASSWORD.
68
79 on:
810 push:
@@ -49,8 +51,46 @@ jobs:
4951 path: budctl-${{ matrix.target }}.tar.gz*
5052 if-no-files-found: error
5153
54+ android:
55+ runs-on: ubuntu-24.04
56+ steps:
57+ - uses: actions/checkout@v6
58+ - uses: actions/setup-java@v6
59+ with:
60+ distribution: temurin
61+ java-version: 21
62+ - uses: dtolnay/rust-toolchain@stable
63+ with:
64+ targets: aarch64-linux-android
65+ - run: cargo install cargo-ndk --locked
66+ - name: Android SDK packages
67+ run: yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;30.0.16248370" "platforms;android-37.0" "build-tools;36.0.0" > /dev/null
68+ - name: The signing key
69+ env:
70+ KEYSTORE: ${{ secrets.BUDCTL_KEYSTORE_BASE64 }}
71+ run: |
72+ test -n "$KEYSTORE" || { echo "the BUDCTL_KEYSTORE_BASE64 secret is not set"; exit 1; }
73+ echo "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/budctl-release.jks"
74+ - name: Build, test and check the APK
75+ working-directory: android
76+ env:
77+ BUDCTL_KEYSTORE: ${{ runner.temp }}/budctl-release.jks
78+ BUDCTL_KEYSTORE_PASSWORD: ${{ secrets.BUDCTL_KEYSTORE_PASSWORD }}
79+ run: |
80+ ./gradlew --no-daemon testReleaseUnitTest assembleRelease
81+ "$ANDROID_HOME/build-tools/36.0.0/apksigner" verify --print-certs app/build/outputs/apk/release/app-release.apk
82+ - name: Package
83+ run: |
84+ cp android/app/build/outputs/apk/release/app-release.apk budctl.apk
85+ sha256sum budctl.apk > budctl.apk.sha256
86+ - uses: actions/upload-artifact@v7
87+ with:
88+ name: android
89+ path: budctl.apk*
90+ if-no-files-found: error
91+
5292 publish:
53− needs: build
93+ needs: [build, android]
5494 runs-on: ubuntu-latest
5595 steps:
5696 - uses: actions/checkout@v6
M CLAUDE.md View
@@ -148,12 +148,13 @@ Exit codes:
148148 ## Releases
149149
150150 - One version for the whole workspace (`Cargo.toml`), with a section per release in `CHANGELOG.md`.
151−- To release: bump the version, add its `CHANGELOG.md` section, commit, then tag `vX.Y.Z` and push the tag. `.github/workflows/release.yml` checks the tag matches `Cargo.toml`, runs the tests, builds x86_64 and aarch64 on Ubuntu 22.04 (glibc builds; Wayland is loaded at runtime), and publishes the tarballs, their `.sha256` files and `install.sh`, with that changelog section as the notes.
151+- To release: bump the version, add its `CHANGELOG.md` section, commit, then tag `vX.Y.Z` and push the tag. `.github/workflows/release.yml` checks the tag matches `Cargo.toml`, runs the tests, builds x86_64 and aarch64 on Ubuntu 22.04 (glibc builds; Wayland is loaded at runtime) and the signed Android APK, and publishes the tarballs, `budctl.apk`, their `.sha256` files and `install.sh`, with that changelog section as the notes.
152+- The APK's signing key is the owner's: `~/.local/share/budctl-release/` (never in the repo) and the repository secrets `BUDCTL_KEYSTORE_BASE64` and `BUDCTL_KEYSTORE_PASSWORD` (`docs/android.md#steps`, step 6). Every release must be signed with that same key, or phones cannot update.
152153 - The tarballs carry the binaries, `budctl.desktop`, the icon (`docs/icon.svg` as `budctl.svg`), `install.sh`, the README and the licenses.
153154 - `install.sh` installs into `~/.local` by default: from a release download (`curl … | sh`), from the unpacked tarball it sits in, or from a checkout with `--source`. It puts the icon in `share/icons/hicolor/scalable/apps/` (a release from before the icon has none, and is installed without). `--uninstall` removes the binaries, the launcher entry and the icon, never the user's config or log.
154155 - `contrib/arch/PKGBUILD` (`budctl-bin`, not on the AUR) packages the release tarballs, the icon too once a release carries it. After a release is published, bump its `pkgver` and take the sums from the release's `.sha256` files.
155156 - The logo: `docs/icon.svg` (the LED battery ring, its last lit dot the red live dot, around a dot-matrix "b") is the launcher icon on both platforms; the Android app has the same dots as vector drawables (adaptive, plus a monochrome layer for themed icons). `docs/logo.svg` is the README's banner: the same emblem and "BUDCTL" in dot-matrix capitals.
156−- `.github/workflows/ci.yml` runs fmt, clippy and the tests on every push to master.
157+- `.github/workflows/ci.yml` runs fmt, clippy and the tests on every push to master, and builds and tests the Android app.
157158
158159 ## License
159160
M README.md View
@@ -160,8 +160,15 @@ your last change or when you leave it, so your computer can use it. It has no in
160160 and its build fails if anything adds one. A switch on its App page turns on the debug log, which you
161161 can copy from there.
162162
163−There is no release build of it yet. To build it and put it on your phone (with USB debugging on),
164−you need Android Studio or the Android SDK with its NDK, the Rust targets
163+To install it, download `budctl.apk` from the
164+[latest release](https://github.com/krolyxon/budctl/releases/latest) on your phone and open it
165+(Android asks you to allow installs from your browser or file manager). New versions come out as
166+new releases: an app such as Obtainium can watch them for you, since budctl itself never checks.
167+The APK is signed with budctl's own key. Its certificate's SHA-256 fingerprint is
168+`45:6F:9B:93:AA:40:1D:57:AA:6C:75:0D:FF:13:FA:82:5A:7B:28:B6:3D:D0:40:99:80:6F:A9:7C:14:75:23:12`.
169+
170+To build it yourself instead and put it on your phone (with USB debugging on), you need Android
171+Studio or the Android SDK with its NDK, the Rust targets
165172 (`rustup target add aarch64-linux-android x86_64-linux-android`) and `cargo install cargo-ndk`:
166173
167174 ```sh
@@ -169,6 +176,9 @@ cd android
169176 ./gradlew installDebug
170177 ```
171178
179+A build of your own is signed with your machine's debug key, so Android will not install a release
180+over it, or the other way round: uninstall one before installing the other.
181+
172182 How it is put together: [`docs/android.md`](docs/android.md). The screenshots show made-up buds
173183 (`adb shell am start -n io.github.krolyxon.budctl/.MainActivity --ez demo true`).
174184
M android/app/build.gradle.kts View
@@ -26,6 +26,20 @@ android {
2626 versionCode = version.split('.').fold(0) { code, part -> code * 100 + part.toInt() }
2727 }
2828
29+ // The release key stays with the owner: its file and password come from the environment (a CI
30+ // secret, or ~/.local/share/budctl-release on the owner's machine), never from the repo.
31+ val keystore = System.getenv("BUDCTL_KEYSTORE")
32+ signingConfigs {
33+ if (keystore != null) {
34+ create("release") {
35+ storeFile = file(keystore)
36+ storePassword = System.getenv("BUDCTL_KEYSTORE_PASSWORD")
37+ keyAlias = "budctl"
38+ keyPassword = System.getenv("BUDCTL_KEYSTORE_PASSWORD")
39+ }
40+ }
41+ }
42+
2943 buildTypes {
3044 // Only where the Rust library is built for: the phone, and the emulator in debug builds.
3145 debug {
@@ -35,6 +49,8 @@ android {
3549 ndk { abiFilters += listOf("arm64-v8a") }
3650 // The Rust library calls Platform's methods by name, so nothing may rename them.
3751 isMinifyEnabled = false
52+ // Without the key, the release build comes out unsigned.
53+ if (keystore != null) signingConfig = signingConfigs.getByName("release")
3854 }
3955 }
4056
M docs/android.md View
@@ -148,10 +148,17 @@ Written from Android's API reference. No other app's code is read for it (rule 2
148148 - Built: the look and every page of the desktop app (Overview, Equalizer, Gestures, Earbud
149149 settings), plus the APP page.
150150 *Done when* **A3** passes for every page.
151−6. **Release.** One version for everything: Gradle reads it from `Cargo.toml`. CI builds and
152− tests the app on every push, and a tag adds a signed APK to the GitHub release. The signing key
153− stays with the owner, as a CI secret. No Play Store. New versions come from the GitHub releases
154− (an app such as Obtainium can watch them); budctl itself never checks.
151+6. **Release.** One version for everything: Gradle reads it from `Cargo.toml` (versionCode
152+ `major * 10000 + minor * 100 + patch`). CI builds and tests the app on every push, and a tag adds
153+ `budctl.apk` and its `.sha256` to the GitHub release. No Play Store. New versions come from the
154+ GitHub releases (an app such as Obtainium can watch them); budctl itself never checks.
155+ - The signing key stays with the owner: `~/.local/share/budctl-release/` on the owner's machine
156+ (the keystore, alias `budctl`, and its password), and the repository's secrets
157+ `BUDCTL_KEYSTORE_BASE64` and `BUDCTL_KEYSTORE_PASSWORD`. Gradle signs release builds when
158+ `BUDCTL_KEYSTORE` and `BUDCTL_KEYSTORE_PASSWORD` are set, and leaves them unsigned otherwise.
159+ Losing the key means a new one, and every phone then has to uninstall before it can update.
160+ - Certificate SHA-256:
161+ `45:6F:9B:93:AA:40:1D:57:AA:6C:75:0D:FF:13:FA:82:5A:7B:28:B6:3D:D0:40:99:80:6F:A9:7C:14:75:23:12`.
155162 *Done when* a tagged release carries the APK and the phone runs it.
156163 7. **Retire the other control app on the phone** (the owner), once **A4** passes.
157164