From 47b2056734e9ebfcf1efc4ce0af871d7768cc1fd Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:06:22 +0200 Subject: [PATCH 1/9] refactor: add abi splitting for android --- composeApp/build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 71d3b924..91e9c5ca 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -364,6 +364,14 @@ android { } } } + splits { + abi { + isEnable = providers.gradleProperty("nuvio.splitAbi").orNull == "true" + reset() + include("arm64-v8a", "armeabi-v7a", "x86_64") + isUniversalApk = false + } + } compileOptions { isCoreLibraryDesugaringEnabled = true sourceCompatibility = JavaVersion.VERSION_11 From af050e2b009d67c59ba110f208511e0d485373e8 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:06:40 +0200 Subject: [PATCH 2/9] ci: add github actions for build check and release --- .github/workflows/build-check.yml | 99 +++++++++++ .github/workflows/release.yml | 274 ++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 .github/workflows/build-check.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 00000000..55d242b5 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,99 @@ +name: Build check + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +# Cancel superseded runs on the same PR. +concurrency: + group: build-check-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + android: + name: Android (compile) + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Compile Android (full + playstore, debug) + run: ./gradlew :composeApp:assembleFullDebug :composeApp:assemblePlaystoreDebug --stacktrace + + ios: + name: iOS (compile) + runs-on: macos-15 + timeout-minutes: 60 + env: + NUVIO_IOS_DISTRIBUTION: full + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Select latest stable Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Make gradlew executable + run: chmod +x ./gradlew + + # Both iOS distributions (appstore / full) swap source dirs and deps, + # so each must be type-checked independently. + - name: Compile shared Kotlin/Native (appstore distribution) + run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=appstore --stacktrace + + - name: Compile shared Kotlin/Native (full distribution) + run: ./gradlew :composeApp:compileKotlinIosArm64 :composeApp:compileKotlinIosSimulatorArm64 -Pnuvio.ios.distribution=full --stacktrace + + - name: Build iOS app (Debug, simulator destination, unsigned) + run: | + set -euo pipefail + xcodebuild \ + -project iosApp/iosApp.xcodeproj \ + -scheme iosApp \ + -configuration Debug \ + -destination 'generic/platform=iOS Simulator' \ + -derivedDataPath build/ios-derived-pr \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + DEVELOPMENT_TEAM="" \ + build + + # Desktop targets (macOS .dmg, Windows .exe, Linux) are not yet wired up in + # composeApp/build.gradle.kts. Add a `jvm("desktop")` target with the Compose + # Desktop plugin and matching `desktop` matrix jobs here once that lands. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f6dbf5c5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,274 @@ +name: Release + +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+' + - 'v[0-9]+.[0-9]+.[0-9]+-*' + +permissions: + contents: write + +jobs: + prepare: + name: Prepare release metadata + runs-on: ubuntu-latest + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + is_prerelease: ${{ steps.version.outputs.is_prerelease }} + steps: + - name: Extract version from tag + id: version + run: | + set -euo pipefail + tag="${GITHUB_REF##*/}" + version="${tag#v}" + if [[ "$version" == *-* ]]; then + is_prerelease=true + else + is_prerelease=false + fi + { + echo "tag=$tag" + echo "version=$version" + echo "is_prerelease=$is_prerelease" + } >>"$GITHUB_OUTPUT" + echo "Tag: $tag (version: $version, prerelease: $is_prerelease)" + + android: + name: Android release + runs-on: ubuntu-latest + needs: prepare + timeout-minutes: 60 + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Configure Android signing if secrets are present + env: + KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }} + KEYSTORE_PASSWORD: ${{ secrets.NUVIO_RELEASE_STORE_PASSWORD }} + KEY_ALIAS: ${{ secrets.NUVIO_RELEASE_KEY_ALIAS }} + KEY_PASSWORD: ${{ secrets.NUVIO_RELEASE_KEY_PASSWORD }} + run: | + set -euo pipefail + if [[ -z "${KEYSTORE_BASE64:-}" ]]; then + echo "::warning::Android release signing secrets are not set — APKs will be unsigned." + exit 0 + fi + keystore_path="$RUNNER_TEMP/nuvio-release.keystore" + printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path" + { + echo "" + echo "NUVIO_RELEASE_STORE_FILE=$keystore_path" + echo "NUVIO_RELEASE_STORE_PASSWORD=$KEYSTORE_PASSWORD" + echo "NUVIO_RELEASE_KEY_ALIAS=$KEY_ALIAS" + echo "NUVIO_RELEASE_KEY_PASSWORD=$KEY_PASSWORD" + } >> local.properties + + - name: Build per-ABI release APKs (full + playstore) + run: | + ./gradlew \ + :composeApp:assembleFullRelease \ + :composeApp:assemblePlaystoreRelease \ + -Pnuvio.splitAbi=true \ + --stacktrace + + - name: Stage and rename APK artifacts + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + mkdir -p artifacts + for flavor in full playstore; do + for abi in arm64-v8a armeabi-v7a x86_64; do + src=$(ls "composeApp/build/outputs/apk/${flavor}/release/composeApp-${flavor}-${abi}-release"*.apk 2>/dev/null | head -n1 || true) + if [[ -z "$src" ]]; then + echo "::error::Missing APK for ${flavor}/${abi}" + ls -la "composeApp/build/outputs/apk/${flavor}/release/" || true + exit 1 + fi + dest="artifacts/Nuvio-${VERSION}-android-${flavor}-${abi}.apk" + cp "$src" "$dest" + echo " $src -> $dest" + done + done + ls -la artifacts/ + + - name: Upload Android APK artifacts + uses: actions/upload-artifact@v4 + with: + name: android-apks + path: artifacts/*.apk + if-no-files-found: error + retention-days: 7 + + ios: + name: iOS release + runs-on: macos-15 + needs: prepare + timeout-minutes: 90 + env: + NUVIO_IOS_DISTRIBUTION: full + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 1 + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Set up Gradle + uses: gradle/actions/setup-gradle@v4 + + - name: Select latest stable Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Make gradlew executable + run: chmod +x ./gradlew + + - name: Archive iOS app (Release, unsigned) + run: | + set -euo pipefail + xcodebuild \ + -project iosApp/iosApp.xcodeproj \ + -scheme iosApp \ + -configuration Release \ + -destination 'generic/platform=iOS' \ + -archivePath build/Nuvio.xcarchive \ + -skipPackagePluginValidation \ + -skipMacroValidation \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY="" \ + DEVELOPMENT_TEAM="" \ + archive + + - name: Repackage .app into unsigned .ipa + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + mkdir -p artifacts build/Payload + app_path="build/Nuvio.xcarchive/Products/Applications/Nuvio.app" + if [[ ! -d "$app_path" ]]; then + echo "::error::Built .app not found at $app_path" + ls -laR build/Nuvio.xcarchive || true + exit 1 + fi + cp -R "$app_path" build/Payload/ + ipa_name="Nuvio-${VERSION}-ios.ipa" + ( + cd build + zip -ry "$ipa_name" Payload >/dev/null + ) + mv "build/$ipa_name" "artifacts/$ipa_name" + ls -la artifacts/ + + - name: Upload iOS IPA artifact + uses: actions/upload-artifact@v4 + with: + name: ios-ipa + path: artifacts/*.ipa + if-no-files-found: error + retention-days: 7 + + release: + name: Publish GitHub release + runs-on: ubuntu-latest + needs: [prepare, android, ios] + timeout-minutes: 15 + steps: + - name: Download Android artifacts + uses: actions/download-artifact@v4 + with: + name: android-apks + path: release-assets/ + + - name: Download iOS artifact + uses: actions/download-artifact@v4 + with: + name: ios-ipa + path: release-assets/ + + - name: List release assets + run: ls -la release-assets/ + + - name: Compose release notes + env: + VERSION: ${{ needs.prepare.outputs.version }} + TAG: ${{ needs.prepare.outputs.tag }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + base="https://github.com/${REPO}/releases/download/${TAG}" + link() { + local file="$1" + local label="$2" + if [[ -f "release-assets/$file" ]]; then + printf '[%s](%s/%s)' "$label" "$base" "$file" + else + printf '—' + fi + } + { + echo "## Nuvio ${VERSION}" + echo + echo "### Downloads" + echo + echo "| Architecture | Android (Full) | Android (Play Store) | iOS |" + echo "| --- | --- | --- | --- |" + printf '| arm64 | %s | %s | %s |\n' \ + "$(link "Nuvio-${VERSION}-android-full-arm64-v8a.apk" "APK")" \ + "$(link "Nuvio-${VERSION}-android-playstore-arm64-v8a.apk" "APK")" \ + "$(link "Nuvio-${VERSION}-ios.ipa" "IPA")" + printf '| armeabi-v7a | %s | %s | — |\n' \ + "$(link "Nuvio-${VERSION}-android-full-armeabi-v7a.apk" "APK")" \ + "$(link "Nuvio-${VERSION}-android-playstore-armeabi-v7a.apk" "APK")" + printf '| x86_64 | %s | %s | — |\n' \ + "$(link "Nuvio-${VERSION}-android-full-x86_64.apk" "APK")" \ + "$(link "Nuvio-${VERSION}-android-playstore-x86_64.apk" "APK")" + echo + echo "### Notes" + echo + echo "- **Android (Full)** is the sideload-distribution flavor with the full feature set. **Android (Play Store)** matches the Play Store-distribution flavor." + echo "- The iOS .ipa is **unsigned** — install with AltStore, Sideloadly, or another sideloading tool." + echo "- Desktop builds (Windows / macOS / Linux) are not yet published from this workflow." + } > release-notes.md + echo "--- release notes ---" + cat release-notes.md + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + name: "Nuvio ${{ needs.prepare.outputs.version }}" + tag_name: ${{ needs.prepare.outputs.tag }} + body_path: release-notes.md + prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }} + files: | + release-assets/*.apk + release-assets/*.ipa + fail_on_unmatched_files: true From 025a8cdee8e334a9d67c84557c1116f1da43ab5f Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:08:17 +0200 Subject: [PATCH 3/9] ci: add temporary workflow dispatch trigger --- .github/workflows/build-check.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 55d242b5..855278a7 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -3,6 +3,7 @@ name: Build check on: pull_request: types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: # TODO: remove before opening upstream PR # Cancel superseded runs on the same PR. concurrency: From 96ba2fa893714cfd25e41e458af53a486796fd3e Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:13:30 +0200 Subject: [PATCH 4/9] fix: remove orphaned libass-android submodule reference It has no entry in .gitmodules, no URL, and nothing in the codebase references it. Also breaks CI --- libass-android | 1 - 1 file changed, 1 deletion(-) delete mode 160000 libass-android diff --git a/libass-android b/libass-android deleted file mode 160000 index c10b71ab..00000000 --- a/libass-android +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c10b71ab8b4d90796d4a795f775d337c29198ad0 From 5a64020d2d159d9dd5aa5da1115888640bf50559 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:21:32 +0200 Subject: [PATCH 5/9] fix: remove orphaned quickjs-kt submodule reference It has no entry in .gitmodules, no URL, and nothing in the codebase references it. Also breaks CI --- vendor/quickjs-kt | 1 - 1 file changed, 1 deletion(-) delete mode 160000 vendor/quickjs-kt diff --git a/vendor/quickjs-kt b/vendor/quickjs-kt deleted file mode 160000 index 57ce0962..00000000 --- a/vendor/quickjs-kt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 57ce096200ac36bceb4e1ee5b6ec411b12357eb8 From e5ec6d418cdee08a6ba332f467326f5547e9da09 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sat, 9 May 2026 23:30:18 +0200 Subject: [PATCH 6/9] fix: missing local.properties file error localProperties file is declared to be optional, respect that decorator --- composeApp/build.gradle.kts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index 91e9c5ca..0515d520 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -178,7 +178,10 @@ val generatedRuntimeConfigDir = layout.buildDirectory.dir("generated/runtime-con val generateRuntimeConfigs = tasks.register("generateRuntimeConfigs") { outputDir.set(generatedRuntimeConfigDir) - localPropertiesFile.set(rootProject.layout.projectDirectory.file("local.properties")) + val localPropsFile = rootProject.file("local.properties") + if (localPropsFile.exists()) { + localPropertiesFile.set(localPropsFile) + } appVersionName.set(releaseAppVersionName) appVersionCode.set(releaseAppVersionCode) } From ffa67820949a23140f1f283cedcc9bb0ed375eb4 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sun, 10 May 2026 00:26:18 +0200 Subject: [PATCH 7/9] ci: add keystore fallback for android --- .github/workflows/release.yml | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6dbf5c5..947fbc4d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -60,7 +60,7 @@ jobs: - name: Make gradlew executable run: chmod +x ./gradlew - - name: Configure Android signing if secrets are present + - name: Configure Android release signing env: KEYSTORE_BASE64: ${{ secrets.NUVIO_RELEASE_KEYSTORE_BASE64 }} KEYSTORE_PASSWORD: ${{ secrets.NUVIO_RELEASE_STORE_PASSWORD }} @@ -68,18 +68,33 @@ jobs: KEY_PASSWORD: ${{ secrets.NUVIO_RELEASE_KEY_PASSWORD }} run: | set -euo pipefail - if [[ -z "${KEYSTORE_BASE64:-}" ]]; then - echo "::warning::Android release signing secrets are not set — APKs will be unsigned." - exit 0 + if [[ -n "${KEYSTORE_BASE64:-}" ]]; then + keystore_path="$RUNNER_TEMP/nuvio-release.keystore" + printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path" + store_password="$KEYSTORE_PASSWORD" + key_alias="$KEY_ALIAS" + key_password="$KEY_PASSWORD" + echo "Using release keystore from secrets." + else + echo "::warning::Release signing secrets not set — generating an ephemeral debug-style keystore so the build can complete. APKs will not be installable over signed production builds." + keystore_path="$RUNNER_TEMP/nuvio-fallback.keystore" + store_password="android" + key_alias="androiddebugkey" + key_password="android" + keytool -genkeypair -v \ + -keystore "$keystore_path" \ + -storepass "$store_password" \ + -keypass "$key_password" \ + -alias "$key_alias" \ + -keyalg RSA -keysize 2048 -validity 10000 \ + -dname "CN=Android Debug,O=Android,C=US" fi - keystore_path="$RUNNER_TEMP/nuvio-release.keystore" - printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$keystore_path" { echo "" echo "NUVIO_RELEASE_STORE_FILE=$keystore_path" - echo "NUVIO_RELEASE_STORE_PASSWORD=$KEYSTORE_PASSWORD" - echo "NUVIO_RELEASE_KEY_ALIAS=$KEY_ALIAS" - echo "NUVIO_RELEASE_KEY_PASSWORD=$KEY_PASSWORD" + echo "NUVIO_RELEASE_STORE_PASSWORD=$store_password" + echo "NUVIO_RELEASE_KEY_ALIAS=$key_alias" + echo "NUVIO_RELEASE_KEY_PASSWORD=$key_password" } >> local.properties - name: Build per-ABI release APKs (full + playstore) From 56efab37f884da031a3c9e72a80098a685684db8 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Sun, 10 May 2026 11:22:08 +0200 Subject: [PATCH 8/9] ci: increase heap space for iOS build --- .github/workflows/release.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 947fbc4d..c17c2cd0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,6 +165,20 @@ jobs: - name: Make gradlew executable run: chmod +x ./gradlew + # The Release Kotlin/Native link of the iOS framework regularly needs + # 8–10 GB of heap (Compose + Ktor + Supabase). Override at the user + # level so we don't have to raise the project default for local devs. + - name: Raise Gradle heap for Kotlin/Native release link + run: | + set -euo pipefail + mkdir -p "$HOME/.gradle" + cat >> "$HOME/.gradle/gradle.properties" <<'EOF' + org.gradle.jvmargs=-Xmx10g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=1g + kotlin.daemon.jvmargs=-Xmx4g + EOF + # Make sure no stale daemon is hanging around with the old args. + ./gradlew --stop || true + - name: Archive iOS app (Release, unsigned) run: | set -euo pipefail From e0c0667e35fcb4f52185c9b835aac93a6f1888d4 Mon Sep 17 00:00:00 2001 From: Marius Butz Date: Tue, 12 May 2026 12:03:06 +0200 Subject: [PATCH 9/9] ci: remove temp manual workflow dispatch --- .github/workflows/build-check.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml index 855278a7..55d242b5 100644 --- a/.github/workflows/build-check.yml +++ b/.github/workflows/build-check.yml @@ -3,7 +3,6 @@ name: Build check on: pull_request: types: [opened, synchronize, reopened, ready_for_review] - workflow_dispatch: # TODO: remove before opening upstream PR # Cancel superseded runs on the same PR. concurrency: