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.
+105 −12
6 files changed
- M .github/workflows/ci.yml +19 −0
- M .github/workflows/release.yml +44 −4
- M CLAUDE.md +3 −2
- M README.md +12 −2
- M android/app/build.gradle.kts +16 −0
- M docs/android.md +11 −4
| @@ -23,3 +23,22 @@ jobs: | |||
| 23 | 23 | - run: cargo clippy -p budctl-core --target aarch64-linux-android --locked -- -D warnings | |
| 24 | 24 | - run: cargo test --workspace --locked | |
| 25 | 25 | - 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 | |
| @@ -1,8 +1,10 @@ | |||
| 1 | 1 | name: Release | |
| 2 | 2 | ||
| 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. | |
| 6 | 8 | ||
| 7 | 9 | on: | |
| 8 | 10 | push: | |
| @@ -49,8 +51,46 @@ jobs: | |||
| 49 | 51 | path: budctl-${{ matrix.target }}.tar.gz* | |
| 50 | 52 | if-no-files-found: error | |
| 51 | 53 | ||
| 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 | + | ||
| 52 | 92 | publish: | |
| 53 | − | needs: build | |
| 93 | + | needs: [build, android] | |
| 54 | 94 | runs-on: ubuntu-latest | |
| 55 | 95 | steps: | |
| 56 | 96 | - uses: actions/checkout@v6 | |
| @@ -148,12 +148,13 @@ Exit codes: | |||
| 148 | 148 | ## Releases | |
| 149 | 149 | ||
| 150 | 150 | - 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. | |
| 152 | 153 | - The tarballs carry the binaries, `budctl.desktop`, the icon (`docs/icon.svg` as `budctl.svg`), `install.sh`, the README and the licenses. | |
| 153 | 154 | - `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. | |
| 154 | 155 | - `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. | |
| 155 | 156 | - 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. | |
| 157 | 158 | ||
| 158 | 159 | ## License | |
| 159 | 160 | ||
| @@ -160,8 +160,15 @@ your last change or when you leave it, so your computer can use it. It has no in | |||
| 160 | 160 | and its build fails if anything adds one. A switch on its App page turns on the debug log, which you | |
| 161 | 161 | can copy from there. | |
| 162 | 162 | ||
| 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 | |
| 165 | 172 | (`rustup target add aarch64-linux-android x86_64-linux-android`) and `cargo install cargo-ndk`: | |
| 166 | 173 | ||
| 167 | 174 | ```sh | |
| @@ -169,6 +176,9 @@ cd android | |||
| 169 | 176 | ./gradlew installDebug | |
| 170 | 177 | ``` | |
| 171 | 178 | ||
| 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 | + | ||
| 172 | 182 | How it is put together: [`docs/android.md`](docs/android.md). The screenshots show made-up buds | |
| 173 | 183 | (`adb shell am start -n io.github.krolyxon.budctl/.MainActivity --ez demo true`). | |
| 174 | 184 | ||
| @@ -26,6 +26,20 @@ android { | |||
| 26 | 26 | versionCode = version.split('.').fold(0) { code, part -> code * 100 + part.toInt() } | |
| 27 | 27 | } | |
| 28 | 28 | ||
| 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 | + | ||
| 29 | 43 | buildTypes { | |
| 30 | 44 | // Only where the Rust library is built for: the phone, and the emulator in debug builds. | |
| 31 | 45 | debug { | |
| @@ -35,6 +49,8 @@ android { | |||
| 35 | 49 | ndk { abiFilters += listOf("arm64-v8a") } | |
| 36 | 50 | // The Rust library calls Platform's methods by name, so nothing may rename them. | |
| 37 | 51 | isMinifyEnabled = false | |
| 52 | + | // Without the key, the release build comes out unsigned. | |
| 53 | + | if (keystore != null) signingConfig = signingConfigs.getByName("release") | |
| 38 | 54 | } | |
| 39 | 55 | } | |
| 40 | 56 | ||
| @@ -148,10 +148,17 @@ Written from Android's API reference. No other app's code is read for it (rule 2 | |||
| 148 | 148 | - Built: the look and every page of the desktop app (Overview, Equalizer, Gestures, Earbud | |
| 149 | 149 | settings), plus the APP page. | |
| 150 | 150 | *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`. | |
| 155 | 162 | *Done when* a tagged release carries the APK and the phone runs it. | |
| 156 | 163 | 7. **Retire the other control app on the phone** (the owner), once **A4** passes. | |
| 157 | 164 | ||