From 7afb60d1b96ea2e9ec57399cd113cefff0fe7385 Mon Sep 17 00:00:00 2001 From: Siddarth Kumar Date: Tue, 20 Jan 2026 16:44:01 +0400 Subject: [PATCH] init: fdroid setup - `mobile/Makefile` -> Added `apk-fdroid` build target, used by `fdroid/build-app.sh`. F-Droid requires an unsigned APK without Google Play services. - `mobile/android/qt6/build.gradle` -> Added F-Droid gradle build flavor, used during `apk-fdroid` target. Separates F-Droid builds from Play Store builds and no signing configuration since it's supposed to be unsigned. - `mobile/scripts/buildApp.sh` -> Updated to copy F-Droid APK instead of usual locations from Jenkins build. - `mobile/scripts/buildOpenSSL.sh` -> Moved from iOS dir, shared between iOS and F-Droid builds. Reusing the OpenSSL build logic that was previously iOS-only but is now needed for F-Droid's from-source requirement. - `mobile/scripts/Common.mk` -> Updated path to `buildOpenSSL.sh` which is no longer inside iOS dir. - `mobile/scripts/openssl-patch.diff` -> Moved from iOS dir, applied by `buildOpenSSL.sh`, so that the patch is co-located with the script that uses it. - `ci/Jenkinsfile.fdroid` -> Had to make a new pipeline script because integrating this into `ci/Jenkinsfile.android` was causing issues with the generated `AppImage` and steps had to be fully sequential, so I figured it's good to separate them out. - `fdroid/entrypoint-fdroid.sh` -> Docker entrypoint, used by `fdroid/Dockerfile`. Sets up the container user/permissions before the F-Droid build starts. - `fdroid/Dockerfile` -> A minimalistic Docker image for the F-Droid Jenkins build environment, used by `ci/Jenkinsfile.fdroid`. I didn't add to existing Qt Android `Dockerfile` because this one only needs a minimal environment with Docker-in-Docker to run the `fdroidserver` VM. - `fdroid/build-app.sh` -> Called by `fdroiddata` metadata build step. Sets up Qt and Android NDK environment, detects `JAVA_HOME`, configures `Go`/`Nim` env vars, and runs the `make` targets for the F-Droid APK. - `fdroid/build-openssl.sh` -> F-Droid requires OpenSSL to be built from source rather than using prebuilt binaries. - `fdroid/build-qt-android.sh` -> F-Droid requires Qt to be built from source; this handles the Android cross-compilation. - `fdroid/build-qt-host.sh` -> Qt's cross-compilation requires host tools (`moc`, `rcc`, etc.) to be built first. - `fdroid/cleanup-binaries.sh` -> Removes binaries flagged by `fdroid` scanner, called by `fdroiddata` metadata `prebuild` step. F-Droid's scanner rejects repos containing prebuilt binaries in test/vendor directories. - `fdroid/cleanup-fdroiddata.sh` -> Removes stale `fdroiddata` clones, called by `ci/Jenkinsfile.fdroid`. Prevents disk space issues from accumulating `fdroiddata` clones across builds. - `fdroid/fdroid-container-build.sh` -> Runs `fdroid build` inside Docker container, called by `scripts/fdroid-local-build.sh`. Isolates the build in a container to match F-Droid's official build environment. - `scripts/fdroid-local-build.sh` -> A handy script to build F-Droid locally, also called by `ci/Jenkinsfile.fdroid`. Manages the full lifecycle of cloning `fdroiddata`, launching the `fdroidserver`, and extracting the output APK. - `fdroid/generate-keystore.sh` -> Generates a single-use keystore for signing the F-Droid APK. Sourced by `buildApp.sh` to export signing credentials into the build environment. fixes: https://github.com/status-im/status-app/issues/19741 --- ci/Jenkinsfile.fdroid | 124 ++++++++++++++++++++ fdroid/Dockerfile | 16 +++ fdroid/build-app.sh | 39 ++++++ fdroid/build-openssl.sh | 27 +++++ fdroid/build-qt.sh | 58 +++++++++ fdroid/cleanup-binaries.sh | 21 ++++ fdroid/cleanup-fdroiddata.sh | 16 +++ fdroid/fdroid-container-build.sh | 81 +++++++++++++ fdroid/generate-keystore.sh | 36 ++++++ fdroid/sign-apk.sh | 44 +++++++ mobile/Makefile | 8 ++ mobile/android/qt6/build.gradle | 8 +- mobile/scripts/Common.mk | 2 +- mobile/scripts/buildApp.sh | 26 +++- mobile/scripts/{ios => }/buildOpenSSL.sh | 29 +++-- mobile/scripts/{ios => }/openssl-patch.diff | 0 scripts/fdroid-local-build.sh | 123 +++++++++++++++++++ 17 files changed, 637 insertions(+), 21 deletions(-) create mode 100644 ci/Jenkinsfile.fdroid create mode 100644 fdroid/Dockerfile create mode 100755 fdroid/build-app.sh create mode 100755 fdroid/build-openssl.sh create mode 100755 fdroid/build-qt.sh create mode 100755 fdroid/cleanup-binaries.sh create mode 100755 fdroid/cleanup-fdroiddata.sh create mode 100755 fdroid/fdroid-container-build.sh create mode 100755 fdroid/generate-keystore.sh create mode 100755 fdroid/sign-apk.sh rename mobile/scripts/{ios => }/buildOpenSSL.sh (81%) rename mobile/scripts/{ios => }/openssl-patch.diff (100%) create mode 100755 scripts/fdroid-local-build.sh diff --git a/ci/Jenkinsfile.fdroid b/ci/Jenkinsfile.fdroid new file mode 100644 index 0000000000..849d504e67 --- /dev/null +++ b/ci/Jenkinsfile.fdroid @@ -0,0 +1,124 @@ +#!/usr/bin/env groovy +library 'status-jenkins-lib@v1.9.40' + +/* Options section can't access functions in objects. */ +def isPRBuild = utils.isPRBuild() + +/* Object to store public URLs for description. */ +descLinks = [:] + +pipeline { + agent { + dockerfile { + dir 'fdroid' + filename 'Dockerfile' + label 'linuxcontainer' + args '--entrypoint="" ' + + '--group-add docker ' + + '--volume=/nix:/nix ' + + '--volume=/etc/nix:/etc/nix ' + + '--volume=/var/run/docker.sock:/var/run/docker.sock ' + } + } + + parameters { + string( + name: 'BUILD_TARGET', + description: 'F-Droid build target in format appid:versioncode (leave empty for script default)', + defaultValue: '' + ) + string( + name: 'FDROIDDATA_REPO', + description: 'Git repository URL for fdroiddata', + defaultValue: 'https://gitlab.com/siddarthkay/fdroiddata.git' // FIXME: point to org repo after https://gitlab.com/fdroid/fdroiddata/-/merge_requests/32193 is merged + ) + string( + name: 'FDROIDDATA_BRANCH', + description: 'Branch of fdroiddata repository to use', + defaultValue: 'app.status.mobile' + ) + } + + options { + timestamps() + /* F-Droid builds can take many hours due to Qt compilation */ + timeout(time: 48, unit: 'HOURS') + /* manage how many builds we keep */ + buildDiscarder(logRotator( + numToKeepStr: '5', + daysToKeepStr: '30', + artifactNumToKeepStr: '2', + )) + /* Allows combined build to copy */ + copyArtifactPermission('/status-app/*') + /* Abort old PR builds. */ + disableConcurrentBuilds(abortPrevious: isPRBuild) + disableRestartFromStage() + } + + environment { + FDROIDDATA_PATH = "${env.WORKSPACE}/fdroiddata" + } + + stages { + + stage('Cleanup Workspace') { + steps { script { + sh './scripts/clean-git.sh' + } } + } + + stage('Clone fdroiddata') { + steps { + sh './fdroid/cleanup-fdroiddata.sh' + sh "git clone --branch '${params.FDROIDDATA_BRANCH}' '${params.FDROIDDATA_REPO}' ${env.FDROIDDATA_PATH}" + /* Update metadata commit to current HEAD so we always build the checked-out revision */ + sh "sed -i 's/commit: .*/commit: '`git rev-parse HEAD`'/' ${env.FDROIDDATA_PATH}/metadata/app.status.mobile.yml" + } + } + + stage('Build F-Droid') { + steps { + sh "./scripts/fdroid-local-build.sh ${params.BUILD_TARGET ?: ''}" + } + } + + stage('Package') { + steps { + script { + sh 'mkdir -p pkg' + sh "cp -v ${env.FDROIDDATA_PATH}/tmp/*.apk pkg/" + env.STATUS_FDROID_APK = utils.findFile("pkg/*.apk") + } + } + } + + stage('Upload') { + steps { + script { + env.PKG_URL = s5cmd.upload(env.STATUS_FDROID_APK) + descLinks['F-Droid APK'] = env.PKG_URL + } + } + } + + stage('Archive') { + steps { + archiveArtifacts artifacts: 'pkg/*.apk', fingerprint: true + } + } + } + + post { + always { script { + jenkins.setBuildDesc(descLinks) + } } + success { script { github.notifyPR(true) } } + failure { script { github.notifyPR(false) } } + cleanup { + sh 'docker ps -aq --filter "name=fdroid-local-build" | xargs -r docker rm -f || true' + sh './fdroid/cleanup-fdroiddata.sh || true' + cleanWs(disableDeferredWipeout: true) + } + } +} diff --git a/fdroid/Dockerfile b/fdroid/Dockerfile new file mode 100644 index 0000000000..9f5428852a --- /dev/null +++ b/fdroid/Dockerfile @@ -0,0 +1,16 @@ +FROM harbor.status.im/infra/ci-build-containers:linux-base-1.0.1 + +USER root + +# Install Docker CE client (newer than docker.io from apt) +RUN apt-get update && apt-get install -yq --no-install-recommends \ + ca-certificates curl gnupg apksigner \ + && install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && chmod a+r /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu jammy stable" > /etc/apt/sources.list.d/docker.list \ + && apt-get update \ + && apt-get install -yq --no-install-recommends docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +USER jenkins diff --git a/fdroid/build-app.sh b/fdroid/build-app.sh new file mode 100755 index 0000000000..bbeb772f0d --- /dev/null +++ b/fdroid/build-app.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -eou pipefail + +QT_VERSION="${QT_VERSION:-6.9.2}" +QT_BASE="$HOME/qt/$QT_VERSION" +ANDROID_ABI="arm64-v8a" +ANDROID_API="${ANDROID_API:-28}" + +if [[ -z "${ANDROID_NDK_ROOT:-}" ]]; then + echo "Error: ANDROID_NDK_ROOT is not set" >&2 + exit 1 +fi + +if [[ -z "${JAVA_HOME:-}" ]]; then + JAVA_HOME="$(dirname "$(dirname "$(readlink -f "$(which javac)")")")" + export JAVA_HOME +fi + +NDK_TOOLCHAIN="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$(uname -s | tr '[:upper:]' '[:lower:]')-x86_64" + +export QT_HOST_PATH="$QT_BASE/gcc_64" +export QTDIR="$QT_BASE/android_arm64_v8a" +export CMAKE_PREFIX_PATH="$QT_BASE/android_arm64_v8a" +export QMAKE="$QT_BASE/android_arm64_v8a/bin/qmake" +export QT_ANDROID_DIR="$QT_BASE/android_arm64_v8a/src/android/java" +export ANDROID_ABI ANDROID_API +export PATH="$QT_BASE/gcc_64/bin:$QT_BASE/android_arm64_v8a/bin:$NDK_TOOLCHAIN/bin:$HOME/go/bin:$PATH" + +cd "$BUILD_DIR" + +go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.34.1 + +ulimit -n 65536 || true +export USE_SYSTEM_NIM=1 +export NIM_SDS_SOURCE_DIR="$BUILD_DIR/vendor/nim-sds" + +make deps + +make -C mobile apk-fdroid BUILD_VARIANT=release ARCH=arm64 V=3 diff --git a/fdroid/build-openssl.sh b/fdroid/build-openssl.sh new file mode 100755 index 0000000000..40fa488563 --- /dev/null +++ b/fdroid/build-openssl.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Build OpenSSL for Qt Android build +# This builds OpenSSL as a dependency for Qt compilation +# (separate from mobile/scripts/buildOpenSSL.sh which builds for the app) +# +set -eo pipefail + +cd "$OPENSSL_SRCDIR" + +if [[ -z "$ANDROID_NDK_ROOT" ]]; then + echo "Error: ANDROID_NDK_ROOT is not set" >&2 + exit 1 +fi + +HOST_OS="$(uname -s | tr '[:upper:]' '[:lower:]')" +export PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/${HOST_OS}-x86_64/bin:$PATH" + +ANDROID_API="${ANDROID_API:-28}" + +./Configure android-arm64 -D__ANDROID_API__="$ANDROID_API" \ + --prefix="$HOME/openssl" \ + --openssldir="$HOME/openssl" \ + no-shared no-tests + +make -j"$(nproc)" 2>&1 | tail -100 +make install_sw > /dev/null diff --git a/fdroid/build-qt.sh b/fdroid/build-qt.sh new file mode 100755 index 0000000000..23e79c964f --- /dev/null +++ b/fdroid/build-qt.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -eo pipefail + +QT_VERSION="${QT_VERSION:-6.9.2}" + +QT_MODULES=qtbase,qtdeclarative,qt5compat,qtmultimedia,qtshadertools,qtimageformats,qtwebview,qtscxml,qtsvg,qtconnectivity,qtwebsockets,qtpositioning,qtlottie,qtwebchannel +(cd "$QT_SRCDIR" && perl init-repository --module-subset="$QT_MODULES") + +# Build Qt for host (required as cross-compilation toolchain for Android) +mkdir -p build_qt_host && cd build_qt_host + +"$QT_SRCDIR"/configure \ + -prefix "$HOME/qt/$QT_VERSION/gcc_64" \ + -release \ + -opensource \ + -confirm-license \ + -nomake examples \ + -nomake tests \ + -- \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_MESSAGE_LOG_LEVEL=WARNING \ + -Wno-dev + +cmake --build . --parallel "$(nproc)" +cmake --install . > /dev/null + +cd "$BUILD_DIR" +rm -rf build_qt_host + +# Build Qt for Android arm64-v8a +mkdir -p build_qt_android && cd build_qt_android + +"$QT_SRCDIR"/configure \ + -prefix "$HOME/qt/$QT_VERSION/android_arm64_v8a" \ + -release \ + -opensource \ + -confirm-license \ + -nomake examples \ + -nomake tests \ + -platform android-clang \ + -android-ndk "$ANDROID_NDK_ROOT" \ + -android-sdk "$ANDROID_SDK_ROOT" \ + -qt-host-path "$HOME/qt/$QT_VERSION/gcc_64" \ + -android-abis arm64-v8a \ + -openssl-linked \ + -- \ + -DOPENSSL_ROOT_DIR="$HOME/openssl" \ + -DCMAKE_MESSAGE_LOG_LEVEL=WARNING \ + -Wno-dev + +cmake --build . --parallel "$(nproc)" +cmake --install . > /dev/null + +cd "$BUILD_DIR" +rm -rf build_qt_android + +find "$QT_SRCDIR" -name "*.o" -delete 2>/dev/null || true +rm -rf "$QT_SRCDIR"/.git "$QT_SRCDIR"/qtwebengine 2>/dev/null || true diff --git a/fdroid/cleanup-binaries.sh b/fdroid/cleanup-binaries.sh new file mode 100755 index 0000000000..27a8c5ab5c --- /dev/null +++ b/fdroid/cleanup-binaries.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -e + +# Remove binary files that F-Droid scanner would flag. +find vendor mobile/vendors -type f -print0 | while IFS= read -r -d '' f; do + if file --mime "$f" | grep -q binary; then + rm -f "$f" + fi +done + +# Target only corpora/corpus subdirs, not the fuzz/ dir itself: OpenSSL's configure +# walks fuzz/build.info and fails with "No such file or directory" if fuzz/ is gone. +find vendor mobile/vendors -type d \( -name 'corpora' -o -name 'corpus' \) -print0 | xargs -0 rm -rf + +# Remove Cargo.toml files without lockfiles that the scanner flags. +find vendor/QR-Code-generator/rust mobile/vendors/openssl/cloudflare-quiche \ + -name "Cargo.toml" -type f -delete + +# Remove test/e2e artefacts not needed for the build. +rm -rf test/e2e +rm -f test/e2e_appium/package.json diff --git a/fdroid/cleanup-fdroiddata.sh b/fdroid/cleanup-fdroiddata.sh new file mode 100755 index 0000000000..cb29a3a8d4 --- /dev/null +++ b/fdroid/cleanup-fdroiddata.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# +# Cleanup fdroiddata directory +# Uses Docker alpine to remove files owned by Docker user (vagrant/root) +# that Jenkins user cannot delete directly +# + +set -e + +FDROIDDATA_PATH="${FDROIDDATA_PATH:-${WORKSPACE}/fdroiddata}" + +if [ -d "$FDROIDDATA_PATH" ]; then + echo "Removing fdroiddata directory: $FDROIDDATA_PATH" + docker run --rm -v "$(dirname "$FDROIDDATA_PATH"):/workspace" alpine:latest \ + sh -c "rm -rf /workspace/$(basename "$FDROIDDATA_PATH")" +fi diff --git a/fdroid/fdroid-container-build.sh b/fdroid/fdroid-container-build.sh new file mode 100755 index 0000000000..1781d77e4e --- /dev/null +++ b/fdroid/fdroid-container-build.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# +# F-Droid Container Build Script +# This script runs inside the fdroidserver Docker container +# Called by fdroid-local-build.sh +# +set -ex + +export home_vagrant=/home/vagrant +export ANDROID_HOME=/opt/android-sdk +export CI_PROJECT_DIR=/fdroiddata + +cd /fdroiddata +chown -R "$(whoami)" . + +for d in logs tmp unsigned "$home_vagrant"/.android "$home_vagrant"/.gradle "$home_vagrant"/metadata "$home_vagrant"/build; do + test -d "$d" || mkdir -p "$d" + chown -R vagrant "$d" 2>/dev/null || true +done + +export fdroidserver=/opt/fdroidserver +rm -rf "$fdroidserver" +mkdir -p "$fdroidserver" +curl -fsSL https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz \ + | tar -xz --directory="$fdroidserver" --strip-components=1 + +export PATH="$fdroidserver:$PATH" +export PYTHONPATH="$fdroidserver:$fdroidserver/examples" +export PYTHONUNBUFFERED=true + +sdkmanager "platform-tools" "build-tools;31.0.0" + +if [[ -d "$home_vagrant/gradlew-fdroid" ]]; then + git -C "$home_vagrant/gradlew-fdroid" pull || true +else + git clone https://gitlab.com/niccokunzmann/gradlew-fdroid.git "$home_vagrant/gradlew-fdroid" || true +fi + +curl -fsSL 'https://gitlab.com/fdroid/fdroid-bootstrap-buildserver/-/raw/master/roles/production_hardening/files/gitconfig' >> /root/.gitconfig + +ln -sf "$home_vagrant/.gradle" /fdroiddata/.gradle 2>/dev/null || true +ln -sf /fdroiddata/tmp "$home_vagrant/tmp" 2>/dev/null || true +ln -sf /fdroiddata/srclibs "$home_vagrant/srclibs" 2>/dev/null || true + +sysctl fs.inotify.max_user_watches=524288 2>/dev/null || true + +export GRADLE_USER_HOME="$home_vagrant/.gradle" + +# Install Java 21 (required for Status app) +apt-get install -y sudo openjdk-21-jdk-headless +update-alternatives --set java /usr/lib/jvm/java-21-openjdk-amd64/bin/java || true + +# Set up fdroid command with vagrant user +fdroid_cmd="sudo --preserve-env --user vagrant \ + env PATH=$fdroidserver:\$PATH \ + env PYTHONPATH=$fdroidserver:$fdroidserver/examples \ + env PYTHONUNBUFFERED=true \ + env TERM=$TERM \ + env HOME=$home_vagrant \ + fdroid" + +appid="${BUILD_TARGET%:*}" +cp -R /fdroiddata/build "$home_vagrant/build" 2>/dev/null || mkdir -p "$home_vagrant/build" +[[ -d "/fdroiddata/metadata/$appid" ]] && cp -R "/fdroiddata/metadata/$appid" "$home_vagrant/metadata/" +cp -R "/fdroiddata/metadata/$appid.yml" "$home_vagrant/metadata/" +chown -R vagrant "$home_vagrant" /fdroiddata + +pushd "$home_vagrant" +ln -sf /fdroiddata "$home_vagrant/fdroiddata" +ln -sf /root/.gitconfig "$home_vagrant/.gitconfig" 2>/dev/null || true + +eval "$fdroid_cmd fetchsrclibs $BUILD_TARGET --verbose" + +rm -f "$home_vagrant/fdroiddata" "$home_vagrant/.gitconfig" + +# unset CI to prevent CI-specific behaviors +(unset CI; eval "$fdroid_cmd build --verbose --test --refresh-scanner --on-server --no-tarball $BUILD_TARGET") + +popd + +ls -la /fdroiddata/tmp/*.apk diff --git a/fdroid/generate-keystore.sh b/fdroid/generate-keystore.sh new file mode 100755 index 0000000000..e4b4c4e872 --- /dev/null +++ b/fdroid/generate-keystore.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -e + +# Generate a single-use keystore for signing the F-Droid APK. +# Intended to be sourced: `source generate-keystore.sh ` +# Exports: FDROID_STORE_FILE, FDROID_STORE_PASSWORD, FDROID_KEY_ALIAS, FDROID_KEY_PASSWORD + +if [[ -z "${1:-}" ]]; then + echo "Usage: source generate-keystore.sh " >&2 + return 1 2>/dev/null || exit 1 +fi + +KEYSTORE_PATH="$1" +FDROID_KEY_ALIAS="status-fdroid" +FDROID_STORE_PASSWORD=$(openssl rand -base64 16) +FDROID_KEY_PASSWORD="$FDROID_STORE_PASSWORD" + +mkdir -p "$(dirname "$KEYSTORE_PATH")" +rm -f "$KEYSTORE_PATH" + +keytool -genkey -v \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -deststoretype pkcs12 \ + -dname "CN=Status, OU=Mobile, O=Status Research, L=Zug, S=Zug, C=CH" \ + -keystore "$KEYSTORE_PATH" \ + -alias "$FDROID_KEY_ALIAS" \ + -storepass "$FDROID_STORE_PASSWORD" \ + -keypass "$FDROID_KEY_PASSWORD" \ + >&2 + +export FDROID_STORE_FILE="$KEYSTORE_PATH" +export FDROID_STORE_PASSWORD +export FDROID_KEY_ALIAS +export FDROID_KEY_PASSWORD diff --git a/fdroid/sign-apk.sh b/fdroid/sign-apk.sh new file mode 100755 index 0000000000..3e45cad71c --- /dev/null +++ b/fdroid/sign-apk.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# +# Sign an unsigned APK with zipalign + apksigner. +# +# Called by mobile/scripts/buildApp.sh when Gradle produces an unsigned APK +# (e.g. fdroid builds where signing configs are stripped by fdroid's +# remove_signing_keys). +# See: https://gitlab.com/fdroid/fdroidserver/-/blob/master/fdroidserver/common.py#L3427 +# +# Required environment variables: +# APK_OUT_UNSIGNED - path to the unsigned APK produced by Gradle +# APK_OUT - desired output path for the signed APK +# ANDROID_HOME - Android SDK root (for build-tools) +# FDROID_STORE_FILE - keystore path +# FDROID_KEY_ALIAS - key alias in the keystore +# FDROID_STORE_PASSWORD - keystore password +# FDROID_KEY_PASSWORD - key password +# +set -eou pipefail + +ZIPALIGN=$(find "$ANDROID_HOME/build-tools" -name zipalign | sort -V | tail -1) +APKSIGNER=$(find "$ANDROID_HOME/build-tools" -name apksigner | sort -V | tail -1) + +if [[ -z "$ZIPALIGN" || -z "$APKSIGNER" ]]; then + echo "Error: zipalign or apksigner not found in ANDROID_HOME=$ANDROID_HOME" >&2 + exit 1 +fi + +APK_DIR="$(dirname "$APK_OUT")" +APK_ALIGNED="${APK_DIR}/$(basename "${APK_OUT_UNSIGNED%.apk}")-aligned.apk" + +"$ZIPALIGN" -f 4 "$APK_OUT_UNSIGNED" "$APK_ALIGNED" + +"$APKSIGNER" sign \ + --ks "$FDROID_STORE_FILE" \ + --ks-key-alias "$FDROID_KEY_ALIAS" \ + --ks-pass "pass:$FDROID_STORE_PASSWORD" \ + --key-pass "pass:$FDROID_KEY_PASSWORD" \ + --out "$APK_OUT" \ + "$APK_ALIGNED" + +"$APKSIGNER" verify "$APK_OUT" +echo "APK signed and verified: $APK_OUT" +rm -f "$APK_ALIGNED" "$APK_OUT_UNSIGNED" diff --git a/mobile/Makefile b/mobile/Makefile index 4a7e388668..0065c5306b 100644 --- a/mobile/Makefile +++ b/mobile/Makefile @@ -227,3 +227,11 @@ clean-status-desktop-rcc: @rm -f $(STATUS_DESKTOP_RCC) @rm -f $(STATUS_DESKTOP)/resources.rcc @rm -f $(STATUS_DESKTOP)/ui/resources.qrc + +.PHONY: apk-fdroid +apk-fdroid: +ifeq ($(OS),android) + @$(MAKE) GRADLE_TARGETS=assembleFdroid $(TARGET) +else + $(error apk-fdroid target is only available for Android) +endif diff --git a/mobile/android/qt6/build.gradle b/mobile/android/qt6/build.gradle index 44289b17a7..437a2a3082 100644 --- a/mobile/android/qt6/build.gradle +++ b/mobile/android/qt6/build.gradle @@ -93,8 +93,8 @@ android { minSdkVersion qtMinSdkVersion targetSdkVersion qtTargetSdkVersion ndk.abiFilters = qtTargetAbiList.split(",") - // Generate unique versionCode based on minutes since epoch - versionCode = (System.currentTimeMillis() / 60000).toInteger() + // Use FDROID_VERSION_CODE env var if set, otherwise generate from timestamp + versionCode = System.getenv("FDROID_VERSION_CODE")?.toInteger() ?: (System.currentTimeMillis() / 60000).toInteger() // Set applicationId and app name based on build variant applicationId = buildVariant == 'release' ? "app.status.mobile" : "app.status.mobile.pr" resValue "string", "app_name", buildVariant == 'release' ? "Status" : "Status PR" @@ -117,6 +117,10 @@ android { minifyEnabled = false signingConfig signingConfigs.release } + fdroid { + debuggable = false + minifyEnabled = false + } debug { debuggable = true applicationIdSuffix ".debug" diff --git a/mobile/scripts/Common.mk b/mobile/scripts/Common.mk index 3116b4d9ee..8dd60924f1 100644 --- a/mobile/scripts/Common.mk +++ b/mobile/scripts/Common.mk @@ -67,7 +67,7 @@ WRAPPER_APP_FILES := $(shell find $(WRAPPER_APP) -type f) STATUS_Q_SCRIPT := $(SCRIPTS_PATH)/buildStatusQ.sh STATUS_GO_SCRIPT := $(SCRIPTS_PATH)/buildStatusGo.sh DOTHERSIDE_SCRIPT := $(SCRIPTS_PATH)/buildDOtherSide.sh -OPENSSL_SCRIPT := $(SCRIPTS_PATH)/ios/buildOpenSSL.sh +OPENSSL_SCRIPT := $(SCRIPTS_PATH)/buildOpenSSL.sh QRCODEGEN_SCRIPT := $(SCRIPTS_PATH)/buildQRCodeGen.sh STATUS_KEYCARD_QT_SCRIPT := $(SCRIPTS_PATH)/buildStatusKeycardQt.sh NIM_STATUS_CLIENT_SCRIPT := $(SCRIPTS_PATH)/buildNimStatusClient.sh diff --git a/mobile/scripts/buildApp.sh b/mobile/scripts/buildApp.sh index 85cc4ec44e..0a988d81d1 100755 --- a/mobile/scripts/buildApp.sh +++ b/mobile/scripts/buildApp.sh @@ -2,6 +2,7 @@ set -eo pipefail CWD=$(realpath "$(dirname "$0")") +REPO_ROOT=$(realpath "$CWD/../..") ARCH=${ARCH:-amd64} SDK=${SDK:-iphonesimulator} @@ -40,6 +41,11 @@ if [[ "${OS}" == "android" ]]; then # Export BUILD_VARIANT for build.gradle to pick up export BUILD_VARIANT + if [[ "$GRADLE_TARGETS" == *"Fdroid"* ]]; then + # shellcheck source=../../fdroid/generate-keystore.sh + source "$REPO_ROOT/fdroid/generate-keystore.sh" "$REPO_ROOT/status-fdroid.keystore" + fi + "$QMAKE_BIN" "$CWD/../wrapperApp/Status.pro" "${QMAKE_CONFIG[@]}" -spec android-clang \ ANDROID_ABIS="${ANDROID_ABI:-arm64-v8a}" VERSION="$VERSION" "${QMAKE_DEFINES[@]}" -after @@ -64,17 +70,31 @@ if [[ "${OS}" == "android" ]]; then # Determine build type from GRADLE_TARGETS if [[ "$GRADLE_TARGETS" == *"Debug"* ]]; then BUILD_TYPE="debug" + elif [[ "$GRADLE_TARGETS" == *"Fdroid"* ]]; then + BUILD_TYPE="fdroid" else BUILD_TYPE="release" fi - # Gradle output paths - APK_OUT="build/outputs/apk/${BUILD_TYPE}/android-build-${BUILD_TYPE}.apk" + # Gradle output paths (may be signed or unsigned depending on environment) + APK_DIR="build/outputs/apk/${BUILD_TYPE}" + export APK_OUT="${APK_DIR}/android-build-${BUILD_TYPE}.apk" + export APK_OUT_UNSIGNED="${APK_DIR}/android-build-${BUILD_TYPE}-unsigned.apk" AAB_OUT="build/outputs/bundle/${BUILD_TYPE}/android-build-${BUILD_TYPE}.aab" # Build with specified gradle targets # shellcheck disable=SC2086 # intentional word splitting for multiple gradle tasks - ./gradlew ${GRADLE_TARGETS} --no-daemon + ./gradlew ${GRADLE_TARGETS} --no-daemon --console=plain + + echo "APK outputs:" + find build/outputs/apk -name '*.apk' 2>/dev/null || echo "No APKs found" + + # If Gradle produced an unsigned APK (e.g. fdroid build where signing configs + # are stripped by fdroid's remove_signing_keys), sign it via the dedicated script. + if [[ ! -f "$APK_OUT" && -f "$APK_OUT_UNSIGNED" && -n "${FDROID_STORE_FILE:-}" ]]; then + echo "Signing unsigned APK..." + "$REPO_ROOT/fdroid/sign-apk.sh" + fi # Copy whichever artifacts were built BUILT="" diff --git a/mobile/scripts/ios/buildOpenSSL.sh b/mobile/scripts/buildOpenSSL.sh similarity index 81% rename from mobile/scripts/ios/buildOpenSSL.sh rename to mobile/scripts/buildOpenSSL.sh index 4aea7bcc45..fdb0ef3a8e 100755 --- a/mobile/scripts/ios/buildOpenSSL.sh +++ b/mobile/scripts/buildOpenSSL.sh @@ -17,7 +17,8 @@ SSL_BUILD_DIR=${BUILD_DIR}/openssl-${TARGET} CRYPTO_OUTPUT_LIB=${LIB_PATH}/libcrypto_3${LIB_EXT} SSL_OUTPUT_LIB=${LIB_PATH}/libssl_3${LIB_EXT} -PLATFORM_ARGS="" +PLATFORM_CONFIG_ARGS=() +PLATFORM_BUILD_ARGS=() if [[ "$OS" == "ios" ]]; then if [[ "$SDK" == "iphonesimulator" ]]; then @@ -38,30 +39,28 @@ if [[ "$OS" == "ios" ]]; then fi if [[ "$OS" == "android" ]]; then - PLATFORM_CONFIG_ARGS="-U__ANDROID_API__ -D__ANDROID_API__=${ANDROID_API}" - PLATFORM_BUILD_ARGS="SHLIB_VERSION_NUMBER=" + PLATFORM_CONFIG_ARGS=("-U__ANDROID_API__" "-D__ANDROID_API__=${ANDROID_API}") + PLATFORM_BUILD_ARGS=("SHLIB_VERSION_NUMBER=") cleanup() { - if [[ "$OS" == "android" ]]; then - patch -d ${OPENSSL} -R -p0 <"${SCRIPT_DIR}/openssl-patch.diff" - fi + patch -d "${OPENSSL}" -R -p0 <"${SCRIPT_DIR}/openssl-patch.diff" } trap cleanup EXIT - patch -d ${OPENSSL} -p0 <"${SCRIPT_DIR}/openssl-patch.diff" + patch -d "${OPENSSL}" -p0 <"${SCRIPT_DIR}/openssl-patch.diff" fi -echo "Building OpenSSL for $TARGET with platform config args $PLATFORM_CONFIG_ARGS" +echo "Building OpenSSL for $TARGET with platform config args ${PLATFORM_CONFIG_ARGS[*]}" -mkdir -p ${SSL_BUILD_DIR} +mkdir -p "${SSL_BUILD_DIR}" ( - cd ${SSL_BUILD_DIR} - + cd "${SSL_BUILD_DIR}" + # - no-module: Makes legacy provider built-in to libcrypto (not a separate module) # - enable-legacy: Enables legacy algorithms including DES # This is required for GlobalPlatform SCP02 which uses single-DES # Reference: https://github.com/openssl/openssl/discussions/25793 - + # Platform-specific config if [[ "$OS" == "ios" ]]; then # iOS uses static libraries (.a files) @@ -70,14 +69,14 @@ mkdir -p ${SSL_BUILD_DIR} # Android uses shared libraries (.so files) SHARED_FLAG="shared" fi - - ${OPENSSL}/Configure --release "$TARGET" $PLATFORM_CONFIG_ARGS \ + + "${OPENSSL}"/Configure --release "$TARGET" "${PLATFORM_CONFIG_ARGS[@]}" \ no-module \ enable-legacy \ enable-des \ enable-md2 \ enable-rc5 \ - $SHARED_FLAG \ + "$SHARED_FLAG" \ no-tests \ no-ui-console # Rebuilding isn't working with the default target, so we need to clean and build again diff --git a/mobile/scripts/ios/openssl-patch.diff b/mobile/scripts/openssl-patch.diff similarity index 100% rename from mobile/scripts/ios/openssl-patch.diff rename to mobile/scripts/openssl-patch.diff diff --git a/scripts/fdroid-local-build.sh b/scripts/fdroid-local-build.sh new file mode 100755 index 0000000000..e8d5956d27 --- /dev/null +++ b/scripts/fdroid-local-build.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -e + +show_help() { + cat << EOF +F-Droid Local Build Script + +Runs the F-Droid build inside the fdroidserver Docker container. + +USAGE: + $0 [OPTIONS] [appid:versioncode] + +OPTIONS: + --no-cleanup Don't remove container after build (useful for debugging) + -h, --help Show this help message + +EXAMPLES: + $0 # Build default (app.status.mobile:29500000) + $0 app.status.mobile:29500000 # Build specific version + $0 --no-cleanup app.status.mobile:29500000 # Keep container after build + +ENVIRONMENT VARIABLES: + FDROIDDATA_PATH Path to fdroiddata repo (default: ./fdroiddata) + +NOTE: + The Status app build requires many hours due to Qt compilation. + Make sure you have sufficient disk space (~100GB recommended). +EOF +} + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +DOCKER_IMAGE="registry.gitlab.com/fdroid/fdroidserver:buildserver-trixie" +FDROIDDATA_PATH="${FDROIDDATA_PATH:-$SCRIPT_DIR/fdroiddata}" +DEFAULT_BUILD="app.status.mobile:29500000" +CONTAINER_NAME="fdroid-local-build-$$" +ENTRYPOINT_SCRIPT="$SCRIPT_DIR/../fdroid/fdroid-container-build.sh" + +log_info() { echo "[INFO] $1"; } +log_error() { echo "[ERROR] $1" >&2; } + +# shellcheck disable=SC2317 # Used by trap +cleanup() { + log_info "Cleaning up container..." + docker rm -f "$CONTAINER_NAME" 2>/dev/null || true +} + +trap cleanup EXIT + +NO_CLEANUP=false +BUILD_TARGET="" + +while [[ $# -gt 0 ]]; do + case $1 in + --no-cleanup) + NO_CLEANUP=true + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -*) + log_error "Unknown option: $1" + show_help + exit 1 + ;; + *) + BUILD_TARGET="$1" + shift + ;; + esac +done + +BUILD_TARGET="${BUILD_TARGET:-$DEFAULT_BUILD}" + +if [[ ! -d "$FDROIDDATA_PATH" ]]; then + log_error "fdroiddata directory not found at: $FDROIDDATA_PATH" + log_error "Set FDROIDDATA_PATH environment variable to the correct path" + exit 1 +fi + +FDROIDDATA_PATH=$(cd "$FDROIDDATA_PATH" && pwd) +log_info "Using fdroiddata at: $FDROIDDATA_PATH" + +APPID="${BUILD_TARGET%:*}" +if [[ ! -f "$FDROIDDATA_PATH/metadata/${APPID}.yml" ]]; then + log_error "Metadata file not found: $FDROIDDATA_PATH/metadata/${APPID}.yml" + exit 1 +fi + +log_info "Build target: $BUILD_TARGET" +log_info "Starting F-Droid build container..." +log_info "This may take a very long time (hours) for complex builds like Status app" +log_info "Container name: $CONTAINER_NAME" + +if $NO_CLEANUP; then + trap - EXIT +fi + +BUILD_EXIT_CODE=0 +docker run -i \ + --name "$CONTAINER_NAME" \ + -v "$FDROIDDATA_PATH:/fdroiddata" \ + -v "$ENTRYPOINT_SCRIPT:/entrypoint.sh:ro" \ + -e "BUILD_TARGET=$BUILD_TARGET" \ + -w /fdroiddata \ + "$DOCKER_IMAGE" \ + /bin/bash /entrypoint.sh || BUILD_EXIT_CODE=$? + +if [[ $BUILD_EXIT_CODE -eq 0 ]]; then + log_info "Build completed successfully!" + log_info "Check $FDROIDDATA_PATH/tmp/ for output files" +else + log_error "Build failed with exit code: $BUILD_EXIT_CODE" + if $NO_CLEANUP; then + log_info "Container preserved for debugging: $CONTAINER_NAME" + log_info "Attach with: docker exec -it $CONTAINER_NAME /bin/bash" + fi +fi + +exit $BUILD_EXIT_CODE