diff --git a/.gitea/workflows/android.yml b/.gitea/workflows/android.yml index ddbf49e..188eec6 100644 --- a/.gitea/workflows/android.yml +++ b/.gitea/workflows/android.yml @@ -22,8 +22,8 @@ jobs: runs-on: musicshare-android container: image: musicshare-android-builder - # Keep Gradle and npm downloads between builds. - options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm + # Keep Gradle (downloads and build cache), npm, and compiled C++ (ccache) between builds. + options: -v musicshare-gradle:/root/.gradle -v musicshare-npm:/root/.npm -v musicshare-ccache:/root/.ccache timeout-minutes: 150 env: SERVER: ${{ github.server_url }} diff --git a/apps/mobile/scripts/build-android.sh b/apps/mobile/scripts/build-android.sh index cf4a547..62216d1 100755 --- a/apps/mobile/scripts/build-android.sh +++ b/apps/mobile/scripts/build-android.sh @@ -7,24 +7,37 @@ # # Both are signed with the upload key when it's configured (see plugins/withReleaseSigning.js; # in CI it comes from ORG_GRADLE_PROJECT_MUSICSHARE_UPLOAD_* variables). Set REQUIRE_SIGNED=1 to -# fail rather than produce unsigned APKs. ABIS picks the CPU types (default: phones, 32 and 64 bit). -# Regenerates apps/mobile/android each time (prebuild --clean). +# fail rather than produce unsigned APKs. ABIS picks the CPU types (default: 64-bit phones, which is +# nearly every phone from the last several years; ABIS=armeabi-v7a,arm64-v8a adds old 32-bit ones). +# Regenerates apps/mobile/android each time (prebuild --clean); Gradle's build cache and ccache (when +# installed) make the rebuild cheap anyway. set -euo pipefail cd "$(dirname "$0")/.." OUT=$(realpath -m "${1:-dist}") VERSION=$(node -p "require('./app.json').expo.version") -ABIS=${ABIS:-armeabi-v7a,arm64-v8a} +ABIS=${ABIS:-arm64-v8a} BUILD_TOOLS=$(ls -d "$ANDROID_HOME"/build-tools/*/ | sort -V | tail -1) mkdir -p "$OUT" say() { printf '\n==> %s\n' "$*"; } +# Most of a build is compiling the C++ in React Native and its libraries, which rarely changes: +# with ccache, CMake (3.17+) reuses earlier results via these launcher variables. +if command -v ccache > /dev/null; then + export CMAKE_C_COMPILER_LAUNCHER=ccache CMAKE_CXX_COMPILER_LAUNCHER=ccache + export CCACHE_BASEDIR=${CCACHE_BASEDIR:-$PWD} CCACHE_COMPILERCHECK=content CCACHE_MAXSIZE=${CCACHE_MAXSIZE:-6G} + ccache --zero-stats > /dev/null +fi + build() { local suffix=$1 foss=$2 say "Building musicshare-$VERSION$suffix.apk" MUSICSHARE_FOSS=$foss CI=1 npx expo prebuild --platform android --clean --no-install - (cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --console=plain) + # --build-cache reuses compiled Kotlin/Java, dex and resources from earlier builds (kept in ~/.gradle). + # Release lint is skipped: it takes a while and TypeScript, ESLint and the tests cover what matters. + (cd android && ./gradlew :app:assembleRelease -PreactNativeArchitectures="$ABIS" --build-cache \ + -x lintVitalAnalyzeRelease -x lintVitalReportRelease -x lintVitalRelease --console=plain) local apk apk=$(ls android/app/build/outputs/apk/release/*.apk | head -1) @@ -56,3 +69,4 @@ build() { build "" 0 build "-foss" 1 (cd "$OUT" && sha256sum musicshare-"$VERSION"*.apk > SHA256SUMS && cat SHA256SUMS) +if command -v ccache > /dev/null; then say "ccache"; ccache --show-stats | grep -iE 'hits|misses|cache size' || true; fi diff --git a/deploy/android-builder/Dockerfile b/deploy/android-builder/Dockerfile index f438644..ca0e1f1 100644 --- a/deploy/android-builder/Dockerfile +++ b/deploy/android-builder/Dockerfile @@ -5,11 +5,11 @@ # # Versions match what React Native 0.86 / Expo 57 ask for # (node_modules/react-native/gradle/libs.versions.toml). Gradle downloads anything -# else it needs on first use; the workflow keeps ~/.gradle in a volume. +# else it needs on first use; the workflow keeps ~/.gradle (and ccache's ~/.ccache) in volumes. FROM node:22-bookworm RUN apt-get update \ - && apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip \ + && apt-get install -y --no-install-recommends openjdk-17-jdk-headless unzip ccache \ && rm -rf /var/lib/apt/lists/* ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \ diff --git a/deploy/gitea-runner/config.yaml b/deploy/gitea-runner/config.yaml index 36db07a..6e97fd9 100644 --- a/deploy/gitea-runner/config.yaml +++ b/deploy/gitea-runner/config.yaml @@ -20,7 +20,8 @@ container: # Use Docker's default network instead of creating one per job: a server with many compose # projects runs out of address pools ("all predefined address pools have been fully subnetted"). network: bridge - # The volumes the workflow may mount (Gradle and npm caches), and nothing else. + # The volumes the workflow may mount (Gradle, npm and ccache caches), and nothing else. valid_volumes: - musicshare-gradle - musicshare-npm + - musicshare-ccache diff --git a/docs/ANDROID-CI.md b/docs/ANDROID-CI.md index 91198e3..98a25bb 100644 --- a/docs/ANDROID-CI.md +++ b/docs/ANDROID-CI.md @@ -20,8 +20,11 @@ The pieces: ## One-time setup (on the Gitea server) -The server needs about 8 GB of free memory during a build and about 15 GB of disk for the image and -caches. A first build takes 30-40 minutes; later ones reuse the Gradle cache. +The server needs about 8 GB of free memory during a build and about 20 GB of disk for the image and +caches. A first build takes 15-40 minutes depending on the machine; later ones reuse the Gradle build +cache and ccache's compiled C++ (volumes `musicshare-gradle` and `musicshare-ccache`) and take a few +minutes. The APKs are for 64-bit phones (`arm64-v8a`); set `ABIS=armeabi-v7a,arm64-v8a` in the workflow's +Build step to include old 32-bit ones. The caches are Docker volumes, which live wherever Docker keeps its data (usually `/var/lib/docker`). Check that disk has room (`df -h /var/lib/docker`): if it fills up, everything else using Docker on the server, including databases, stops writing.