From 88277cf718604f552188ecab64ee00b51d109003 Mon Sep 17 00:00:00 2001 From: Suman Biswas Date: Fri, 9 Oct 2026 22:51:47 +0530 Subject: [PATCH] build(mob): prepare Android app for F-Droid - Sign release builds from key.properties or ELEPHANT_* env vars (CI secrets); otherwise leave them unsigned for F-Droid. The debug key is only used when CI explicitly allows it. - Disable AGP's dependency-info block, which F-Droid rejects - Drop the "FOSS sanitizer" task: R8 already removes Flutter's Play Store classes, and fdroid scanner finds no non-free code in the APKs - Use Flutter's per-ABI versionCodes (1000/2000/4000 + build number) instead of stacking a second scheme on top of it - Bump version to 0.3.1+3 (it was stuck at 0.2.0+2 since v0.2.0) - Move launcher_name and flutter_launcher_icons to dev_dependencies - CI: pin Flutter 3.47.2, enforce pubspec.lock, fail when the tag doesn't match pubspec.yaml, sign with ANDROID_KEYSTORE_* secrets - Store listing: rename to full_description.txt and changelogs/ (the names F-Droid reads), rewrite the texts, add 4003.txt changelog - Add the fdroiddata build recipe and a release guide under fdroid/ Refs #156 --- .github/workflows/android-release.yaml | 34 +++++++- fdroid/README.md | 60 +++++++++++++++ fdroid/in.commandlinecoding.elephant.yml | 94 +++++++++++++++++++++++ metadata/en-US/chagelogs/23.txt | 4 - metadata/en-US/changelogs/4003.txt | 7 ++ metadata/en-US/full_description.txt | 27 +++++++ metadata/en-US/long_description.txt | 3 - metadata/en-US/short_description.txt | 2 +- mobile/android/app/build.gradle.kts | 84 ++++++++++---------- mobile/lib/pages/settings/help_about.dart | 4 +- mobile/pubspec.lock | 4 +- mobile/pubspec.yaml | 10 ++- 12 files changed, 271 insertions(+), 62 deletions(-) create mode 100644 fdroid/README.md create mode 100644 fdroid/in.commandlinecoding.elephant.yml delete mode 100644 metadata/en-US/chagelogs/23.txt create mode 100644 metadata/en-US/changelogs/4003.txt create mode 100644 metadata/en-US/full_description.txt delete mode 100644 metadata/en-US/long_description.txt diff --git a/.github/workflows/android-release.yaml b/.github/workflows/android-release.yaml index 38a80c6..0f186f3 100644 --- a/.github/workflows/android-release.yaml +++ b/.github/workflows/android-release.yaml @@ -26,15 +26,47 @@ jobs: distribution: temurin java-version: '25' + # Pinned so release APKs and F-Droid's build (which reads this line) + # use the same Flutter version. - name: Set up Flutter uses: subosito/flutter-action@v2 with: channel: stable + flutter-version: '3.47.2' cache: true + - name: Check the tag matches pubspec.yaml + working-directory: mobile + run: | + version=$(sed -n -E 's/^version:[[:space:]]*([^+]+)\+.*/\1/p' pubspec.yaml) + if [ "v$version" != "${GITHUB_REF_NAME}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} doesn't match pubspec.yaml version $version. Bump version (and its +build number) before tagging." + exit 1 + fi + - name: Install Flutter dependencies working-directory: mobile - run: flutter pub get + run: flutter pub get --enforce-lockfile + + - name: Set up release signing + env: + KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }} + run: | + if [ -z "$KEYSTORE_BASE64" ]; then + echo "::warning::ANDROID_KEYSTORE_BASE64 is not set; APKs are signed with a throwaway debug key and can't update each other." + echo "ELEPHANT_ALLOW_DEBUG_SIGNING=true" >> "$GITHUB_ENV" + exit 0 + fi + echo "$KEYSTORE_BASE64" | base64 --decode > "$RUNNER_TEMP/release.jks" + { + echo "ELEPHANT_KEYSTORE_PATH=$RUNNER_TEMP/release.jks" + echo "ELEPHANT_KEYSTORE_PASSWORD=$KEYSTORE_PASSWORD" + echo "ELEPHANT_KEY_ALIAS=$KEY_ALIAS" + echo "ELEPHANT_KEY_PASSWORD=$KEY_PASSWORD" + } >> "$GITHUB_ENV" - name: Run Flutter analyze working-directory: mobile diff --git a/fdroid/README.md b/fdroid/README.md new file mode 100644 index 0000000..717ad72 --- /dev/null +++ b/fdroid/README.md @@ -0,0 +1,60 @@ +# F-Droid + +[`in.commandlinecoding.elephant.yml`](in.commandlinecoding.elephant.yml) is the build recipe to submit to [fdroiddata](https://gitlab.com/fdroid/fdroiddata). F-Droid builds the app from source with that recipe and signs it with its own key. The store listing (description, changelogs, icon, screenshots) is read from [`metadata/en-US/`](../metadata/en-US/) in this repository. + +## Releasing a new version + +F-Droid checks for new releases by reading `mobile/pubspec.yaml` at each `v*` tag, so the version there must be bumped before tagging. + +1. Bump `version:` in `mobile/pubspec.yaml`. Increase both parts: `0.3.1+3` → `0.3.2+4`. The number after `+` is the versionCode and must always go up. +2. Add a changelog at `metadata/en-US/changelogs/<4000 + versionCode>.txt` (for `+4`, that's `4004.txt`). Keep it under 500 characters. +3. Commit, then tag `v` (e.g. `v0.3.2`) and push the tag. The release workflow fails if the tag doesn't match `pubspec.yaml`. + +F-Droid picks up the new tag automatically within a few days. + +## Version codes + +APKs are built per ABI, and Flutter derives each versionCode from the `pubspec.yaml` build number: + +| ABI | versionCode | +|---|---| +| armeabi-v7a | 1000 + build number | +| arm64-v8a | 2000 + build number | +| x86_64 | 4000 + build number | + +The recipe's `VercodeOperation` mirrors this. If a Flutter upgrade changes this scheme, update the recipe. + +## Flutter version + +F-Droid builds with the Flutter version pinned as `flutter-version` in [`.github/workflows/android-release.yaml`](../.github/workflows/android-release.yaml). To upgrade Flutter, change it there; both the GitHub release and F-Droid follow. + +## Release signing (GitHub releases) + +F-Droid signs its own builds; this only affects the APKs attached to GitHub releases. Add these repository secrets so every release is signed with the same key and users can update from one release to the next: + +| Secret | Value | +|---|---| +| `ANDROID_KEYSTORE_BASE64` | `base64 -w0 release.jks` | +| `ANDROID_KEYSTORE_PASSWORD` | Keystore password | +| `ANDROID_KEY_ALIAS` | Key alias | +| `ANDROID_KEY_PASSWORD` | Key password | + +Without them, the workflow falls back to a throwaway debug key and prints a warning. + +For local release builds, create `mobile/android/key.properties` (git-ignored): + +```properties +storeFile=/absolute/path/to/release.jks +storePassword=... +keyAlias=... +keyPassword=... +``` + +Without `key.properties` or the secrets, release builds are unsigned, which is what F-Droid expects. + +## Submitting to F-Droid + +1. Tag the release (see above). +2. Fork [fdroiddata](https://gitlab.com/fdroid/fdroiddata) and copy the recipe to `metadata/in.commandlinecoding.elephant.yml`. +3. Replace `commit: v0.3.1` in each build with the tag's full commit hash. +4. Run `fdroid lint in.commandlinecoding.elephant` and `fdroid build -v -l in.commandlinecoding.elephant`, then open a merge request. diff --git a/fdroid/in.commandlinecoding.elephant.yml b/fdroid/in.commandlinecoding.elephant.yml new file mode 100644 index 0000000..c19eb8b --- /dev/null +++ b/fdroid/in.commandlinecoding.elephant.yml @@ -0,0 +1,94 @@ +Categories: + - Internet + - Messaging +License: AGPL-3.0-only +AuthorName: CommandLineCoding +WebSite: https://github.com/CommandLineCoding/Elephant +SourceCode: https://github.com/CommandLineCoding/Elephant +IssueTracker: https://github.com/CommandLineCoding/Elephant/issues +Changelog: https://github.com/CommandLineCoding/Elephant/releases + +AutoName: Elephant + +RepoType: git +Repo: https://github.com/CommandLineCoding/Elephant.git + +Builds: + - versionName: 0.3.1 + versionCode: 1003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm" + ndk: 28.2.13676358 + + - versionName: 0.3.1 + versionCode: 2003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-arm64" + ndk: 28.2.13676358 + + - versionName: 0.3.1 + versionCode: 4003 + commit: v0.3.1 + subdir: mobile + output: build/app/outputs/flutter-apk/app-x86_64-release.apk + srclibs: + - flutter@stable + rm: + - mobile/ios + prebuild: + - flutterVersion=$(sed -n -E "s/.*flutter-version:\ '(.*)'/\1/p" ../.github/workflows/android-release.yaml) + - '[[ $flutterVersion ]]' + - git -C $$flutter$$ checkout -f $flutterVersion + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter config --no-analytics + - $$flutter$$/bin/flutter pub get --enforce-lockfile + scandelete: + - mobile/.pub-cache + build: + - export PUB_CACHE=$(pwd)/.pub-cache + - $$flutter$$/bin/flutter build apk --release --split-per-abi --target-platform="android-x64" + ndk: 28.2.13676358 + +AutoUpdateMode: Version +UpdateCheckMode: Tags +VercodeOperation: + - '%c + 1000' + - '%c + 2000' + - '%c + 4000' +UpdateCheckData: mobile/pubspec.yaml|version:\s.+\+(\d+)|.|version:\s(.+)\+ +CurrentVersion: 0.3.1 +CurrentVersionCode: 4003 diff --git a/metadata/en-US/chagelogs/23.txt b/metadata/en-US/chagelogs/23.txt deleted file mode 100644 index 98cfe2d..0000000 --- a/metadata/en-US/chagelogs/23.txt +++ /dev/null @@ -1,4 +0,0 @@ -- Added user authentication with login and registration -- Support for username and password-based accounts -- Added theme customization options -- Initial release on F-Droid \ No newline at end of file diff --git a/metadata/en-US/changelogs/4003.txt b/metadata/en-US/changelogs/4003.txt new file mode 100644 index 0000000..6cd9e1d --- /dev/null +++ b/metadata/en-US/changelogs/4003.txt @@ -0,0 +1,7 @@ +First release on F-Droid. +- End-to-end encrypted direct messages (Signal Protocol) +- Group chats, replies, typing indicators and read receipts +- Encrypted on-device message storage and offline sending +- Username/password, Google or GitHub sign-in +- Connect to Elephant Cloud or your own server +- Four themes diff --git a/metadata/en-US/full_description.txt b/metadata/en-US/full_description.txt new file mode 100644 index 0000000..4755407 --- /dev/null +++ b/metadata/en-US/full_description.txt @@ -0,0 +1,27 @@ +Elephant is a privacy-first, open-source messenger. Direct messages are end-to-end encrypted with the Signal Protocol: your private keys never leave your device, and the server only ever stores public keys and encrypted text. + +Features + + +Your server, your data +Elephant connects to the public server at elephant.commandlinecoding.in by default. The server is also free software (AGPL-3.0), and you can run your own and point the app at it from the sign-in screen. + +Good to know + + +Source code, server setup and issue tracker: https://github.com/CommandLineCoding/Elephant diff --git a/metadata/en-US/long_description.txt b/metadata/en-US/long_description.txt deleted file mode 100644 index 3b3a24c..0000000 --- a/metadata/en-US/long_description.txt +++ /dev/null @@ -1,3 +0,0 @@ -Elephant is a privacy-first messaging application made with Go and Flutter. -It supports secure authentication, user discovery, and real-time chat over WebSocket, -with all communication TLS-protected between client and server. \ No newline at end of file diff --git a/metadata/en-US/short_description.txt b/metadata/en-US/short_description.txt index 61d94bf..5bb4d35 100644 --- a/metadata/en-US/short_description.txt +++ b/metadata/en-US/short_description.txt @@ -1 +1 @@ -Elephant – a privacy-first, open-source messenger. \ No newline at end of file +Private messenger with end-to-end encrypted chats that you can self-host diff --git a/mobile/android/app/build.gradle.kts b/mobile/android/app/build.gradle.kts index b5832bb..fc06d0a 100644 --- a/mobile/android/app/build.gradle.kts +++ b/mobile/android/app/build.gradle.kts @@ -1,9 +1,25 @@ +import java.util.Properties + plugins { id("com.android.application") id("org.jetbrains.kotlin.android") id("dev.flutter.flutter-gradle-plugin") } +// Release signing comes from android/key.properties (never committed) or the +// equivalent ELEPHANT_* environment variables in CI. Without either, release +// builds are unsigned, which is what F-Droid expects: it signs with its own key. +val keystoreProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.exists()) file.inputStream().use { load(it) } +} + +fun signingValue(key: String, env: String): String? = + keystoreProperties.getProperty(key) ?: System.getenv(env) + +val releaseStoreFile = signingValue("storeFile", "ELEPHANT_KEYSTORE_PATH") + +// Keep Google Play libraries out of the dependency graph (F-Droid inclusion policy). configurations.all { exclude(group = "com.google.android.play", module = "core") exclude(group = "com.google.android.play", module = "core-common") @@ -23,6 +39,24 @@ android { targetCompatibility = JavaVersion.VERSION_17 } + signingConfigs { + if (releaseStoreFile != null) { + create("release") { + storeFile = file(releaseStoreFile) + storePassword = signingValue("storePassword", "ELEPHANT_KEYSTORE_PASSWORD") + keyAlias = signingValue("keyAlias", "ELEPHANT_KEY_ALIAS") + keyPassword = signingValue("keyPassword", "ELEPHANT_KEY_PASSWORD") + } + } + } + + // AGP otherwise embeds a dependency report encrypted with Google's key, + // which F-Droid's scanner rejects and which breaks reproducible builds. + dependenciesInfo { + includeInApk = false + includeInBundle = false + } + defaultConfig { applicationId = "in.commandlinecoding.elephant" minSdk = flutter.minSdkVersion @@ -33,7 +67,8 @@ android { buildTypes { getByName("release") { - signingConfig = signingConfigs.getByName("debug") + // Kept on one line: F-Droid's build strips `signingConfig` lines. + signingConfig = signingConfigs.findByName("release") ?: signingConfigs.getByName("debug").takeIf { System.getenv("ELEPHANT_ALLOW_DEBUG_SIGNING") == "true" } isMinifyEnabled = true isShrinkResources = true proguardFiles( @@ -54,49 +89,8 @@ tasks.withType().configureEach } } -val abiCodes = mapOf("armeabi-v7a" to 1, "arm64-v8a" to 2, "x86_64" to 3) - -androidComponents { - onVariants { variant -> - variant.outputs.forEach { output -> - val abi = output.filters.find { - it.filterType == com.android.build.api.variant.FilterConfiguration.FilterType.ABI - }?.identifier - - val baseAbiCode = abiCodes[abi] - if (baseAbiCode != null) { - val baseVersionCode = flutter.versionCode - output.versionCode.set(baseVersionCode * 10 + baseAbiCode) - } - } - } -} - -project.afterEvaluate { - tasks.configureEach { - if (name.contains("minifyReleaseWithR8") || name.contains("minifyReleaseWithProguard") || name.contains("dexBuilderRelease")) { - doFirst { - logger.lifecycle("FOSS SANITIZER: Active. Sweeping intermediate files for non-free binaries...") - - val searchDirs = listOf( - File(project.layout.buildDirectory.asFile.get(), "intermediates/classes/release"), - File(project.layout.buildDirectory.asFile.get(), "intermediates/javac/release") - ) - - for (dir in searchDirs) { - if (dir.exists()) { - dir.walkBottomUp().forEach { file -> - val normalizedPath = file.absolutePath.replace('\\', '/') - if (file.isDirectory && normalizedPath.endsWith("com/google/android/play/core")) { - file.deleteRecursively() - logger.lifecycle("Successfully removed vendor-shaded tracking package: $normalizedPath") - } - } - } - } - } - } - } -} +// Per-ABI versionCodes come from Flutter itself with --split-per-abi: +// 1000 + base (armeabi-v7a), 2000 + base (arm64-v8a), 4000 + base (x86_64). +// fdroid/in.commandlinecoding.elephant.yml depends on this scheme. dependencies {} \ No newline at end of file diff --git a/mobile/lib/pages/settings/help_about.dart b/mobile/lib/pages/settings/help_about.dart index 1c0071c..2b16c5d 100644 --- a/mobile/lib/pages/settings/help_about.dart +++ b/mobile/lib/pages/settings/help_about.dart @@ -59,7 +59,7 @@ class HelpAboutPage extends StatelessWidget { ), Center( child: Text( - 'Version 0.3.0 - Secure Messaging', + 'Version 0.3.1 - Secure Messaging', style: TextStyle( fontSize: 14, color: theme.colorScheme.onSurfaceVariant, @@ -103,7 +103,7 @@ class HelpAboutPage extends StatelessWidget { // --- KNOWN BUGS SECTION --- _buildSectionHeader( context, - 'Known Bugs & V0.3.0 Limitations', + 'Known Bugs & V0.3.1 Limitations', Icons.bug_report, ), Card( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5d604d8..82e0957 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -231,7 +231,7 @@ packages: source: hosted version: "4.5.2" flutter_launcher_icons: - dependency: "direct main" + dependency: "direct dev" description: name: flutter_launcher_icons sha256: "10f13781741a2e3972126fae08393d3c4e01fa4cd7473326b94b72cf594195e7" @@ -401,7 +401,7 @@ packages: source: hosted version: "4.12.0" launcher_name: - dependency: "direct main" + dependency: "direct dev" description: name: launcher_name sha256: "5fc9a8b8de9e255d5f21effc33632b5620771c9b2310d459a7710b725353b305" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 4e566b6..46d61a3 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -1,5 +1,5 @@ name: mobile -description: "A new Flutter project." +description: "Elephant: a privacy-first, end-to-end encrypted messenger." # The following line prevents the package from being accidentally published to # pub.dev using `flutter pub publish`. This is preferred for private packages. publish_to: "none" # Remove this line if you wish to publish to pub.dev @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.2.0+2 +version: 0.3.1+3 environment: sdk: ^3.12.0 @@ -33,8 +33,6 @@ dependencies: web_socket_channel: ^3.0.3 provider: ^6.1.5+1 - launcher_name: ^1.0.2 - flutter_launcher_icons: ^0.14.4 flutter_secure_storage: ^9.2.2 dio: ^5.11.0 flutter_markdown_plus: ^1.0.12 @@ -67,6 +65,10 @@ dev_dependencies: flutter_test: sdk: flutter + # Build-time generators for the launcher name and icons (configured above). + launcher_name: ^1.0.2 + flutter_launcher_icons: ^0.14.4 + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your