Compare commits

...

2 Commits

7 changed files with 1625 additions and 59 deletions

View File

@ -10,9 +10,9 @@ jobs:
runs-on: macOS-latest runs-on: macOS-latest
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: 'zulu' distribution: 'zulu'
java-version: 21 java-version: 21

View File

@ -27,8 +27,8 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview COMPOSE_WEBVIEW_REPO: KRTirtho/ComposeNativeWebview
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce COMPOSE_WEBVIEW_COMMIT: cbd36f88927298a28a2d64c6acf7ec3b2def6f9d
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin
@ -38,7 +38,7 @@ jobs:
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- name: Checkout ComposeNativeWebview - name: Checkout ComposeNativeWebview
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
repository: ${{ env.COMPOSE_WEBVIEW_REPO }} repository: ${{ env.COMPOSE_WEBVIEW_REPO }}
ref: ${{ env.COMPOSE_WEBVIEW_COMMIT }} ref: ${{ env.COMPOSE_WEBVIEW_COMMIT }}
@ -46,33 +46,33 @@ jobs:
path: compose-webview path: compose-webview
- name: Checkout gradle-plugin - name: Checkout gradle-plugin
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
repository: ${{ env.GRADLE_PLUGIN_REPO }} repository: ${{ env.GRADLE_PLUGIN_REPO }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
path: gradle-plugin path: gradle-plugin
- name: Checkout vlcj-bundler - name: Checkout vlcj-bundler
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
repository: ${{ env.VLCJ_BUNDLER_REPO }} repository: ${{ env.VLCJ_BUNDLER_REPO }}
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
path: vlcj-bundler path: vlcj-bundler
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
- name: Set up Android SDK - name: Set up Android SDK
uses: android-actions/setup-android@v3 uses: android-actions/setup-android@v4
- name: Set up Rust - name: Set up Rust
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Publish compose-webview to mavenLocal - name: Publish compose-webview to mavenLocal
working-directory: compose-webview working-directory: compose-webview
@ -93,7 +93,7 @@ jobs:
./gradlew publishToMavenLocal ./gradlew publishToMavenLocal
- name: Upload maven local - name: Upload maven local
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
@ -103,16 +103,16 @@ jobs:
needs: prepare-deps needs: prepare-deps
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
@ -121,7 +121,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
@ -154,7 +154,7 @@ jobs:
cp "$APK_FILE" Spotube-android-all-arch.apk cp "$APK_FILE" Spotube-android-all-arch.apk
- name: Upload APK - name: Upload APK
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: android-apk name: android-apk
path: Spotube-android-all-arch.apk path: Spotube-android-all-arch.apk
@ -170,10 +170,10 @@ jobs:
needs: prepare-deps needs: prepare-deps
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
@ -181,10 +181,10 @@ jobs:
- name: Install packaging tools - name: Install packaging tools
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y rpm fuse sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
@ -193,29 +193,35 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v6
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package Linux - name: Package Linux
run: ./gradlew :composeApp:packageReleaseDeb :composeApp:packageReleaseRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }} run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Linux artifacts - name: Rename Linux artifacts
run: | run: |
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1) DEB_FILE=$(find composeApp/build/compose/binaries/main/graalvm-deb -maxdepth 1 -name "*.deb" -type f | head -1)
RPM_FILE=$(find composeApp/build -name "*.rpm" -type f | head -1) RPM_FILE=$(find composeApp/build/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
cp "$DEB_FILE" Spotube-linux-x86_64.deb cp "$DEB_FILE" Spotube-linux-x86_64.deb
cp "$RPM_FILE" Spotube-linux-x86_64.rpm cp "$RPM_FILE" Spotube-linux-x86_64.rpm
- name: Upload DEB - name: Upload DEB
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: linux-deb name: linux-deb
path: Spotube-linux-x86_64.deb path: Spotube-linux-x86_64.deb
- name: Upload RPM - name: Upload RPM
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: linux-rpm name: linux-rpm
path: Spotube-linux-x86_64.rpm path: Spotube-linux-x86_64.rpm
@ -231,10 +237,10 @@ jobs:
needs: prepare-deps needs: prepare-deps
runs-on: ubuntu-24.04-arm runs-on: ubuntu-24.04-arm
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
@ -242,10 +248,10 @@ jobs:
- name: Install packaging tools - name: Install packaging tools
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y rpm fuse sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
@ -254,29 +260,35 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v6
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package Linux - name: Package Linux
run: ./gradlew :composeApp:packageReleaseDeb :composeApp:packageReleaseRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }} run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Linux artifacts - name: Rename Linux artifacts
run: | run: |
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1) DEB_FILE=$(find composeApp/build/compose/binaries/main/graalvm-deb -maxdepth 1 -name "*.deb" -type f | head -1)
RPM_FILE=$(find composeApp/build -name "*.rpm" -type f | head -1) RPM_FILE=$(find composeApp/build/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
cp "$DEB_FILE" Spotube-linux-aarch64.deb cp "$DEB_FILE" Spotube-linux-aarch64.deb
cp "$RPM_FILE" Spotube-linux-aarch64.rpm cp "$RPM_FILE" Spotube-linux-aarch64.rpm
- name: Upload DEB - name: Upload DEB
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: linux-deb-arm64 name: linux-deb-arm64
path: Spotube-linux-aarch64.deb path: Spotube-linux-aarch64.deb
- name: Upload RPM - name: Upload RPM
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: linux-rpm-arm64 name: linux-rpm-arm64
path: Spotube-linux-aarch64.rpm path: Spotube-linux-aarch64.rpm
@ -292,16 +304,16 @@ jobs:
needs: prepare-deps needs: prepare-deps
runs-on: windows-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
@ -310,19 +322,31 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
# native-image links with MSVC
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Cache GraalVM toolchain
uses: actions/cache@v6
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package EXE - name: Package EXE
run: ./gradlew :composeApp:packageReleaseExe -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }} run: ./gradlew :composeApp:packageGraalvmExe -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Windows artifacts - name: Rename Windows artifacts
shell: pwsh shell: pwsh
run: | run: |
$EXE_FILE = Get-ChildItem -Path composeApp/build -Filter "*.exe" -Recurse | Select-Object -First 1 # Top level only: subfolders hold the unpacked app, and __* are electron-builder helpers
$EXE_FILE = Get-ChildItem -Path composeApp/build/compose/binaries/main/graalvm-exe -Filter "*.exe" |
Where-Object { $_.Name -notlike '__*' } | Select-Object -First 1
Copy-Item $EXE_FILE.FullName Spotube-windows-x86_64-setup.exe Copy-Item $EXE_FILE.FullName Spotube-windows-x86_64-setup.exe
- name: Upload EXE - name: Upload EXE
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: windows-exe name: windows-exe
path: Spotube-windows-x86_64-setup.exe path: Spotube-windows-x86_64-setup.exe
@ -338,16 +362,16 @@ jobs:
needs: prepare-deps needs: prepare-deps
runs-on: macos-latest runs-on: macos-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
@ -356,21 +380,27 @@ jobs:
uses: dtolnay/rust-toolchain@stable uses: dtolnay/rust-toolchain@stable
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v6
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package DMG - name: Package DMG
run: ./gradlew :composeApp:packageReleaseDmg -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }} run: ./gradlew :composeApp:packageGraalvmDmg -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename macOS artifact - name: Rename macOS artifact
run: | run: |
DMG_FILE=$(find composeApp/build -name "*.dmg" -type f | head -1) DMG_FILE=$(find composeApp/build/compose/binaries/main/graalvm-dmg -maxdepth 1 -name "*.dmg" -type f | head -1)
cp "$DMG_FILE" Spotube-macos-universal.dmg cp "$DMG_FILE" Spotube-macos-universal.dmg
- name: Upload DMG - name: Upload DMG
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: macos-dmg name: macos-dmg
path: Spotube-macos-universal.dmg path: Spotube-macos-universal.dmg
@ -392,23 +422,23 @@ jobs:
- build-macos - build-macos
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
# The build resolves its plugins from mavenLocal, so Gradle needs the same setup as the build jobs # The build resolves its plugins from mavenLocal, so Gradle needs the same setup as the build jobs
- name: Download maven local - name: Download maven local
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: maven-local name: maven-local
path: ~/.m2/repository/ path: ~/.m2/repository/
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v6
with: with:
distribution: temurin distribution: temurin
java-version: 21 java-version: 21
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v6
- name: Set release variables - name: Set release variables
id: vars id: vars
@ -430,7 +460,7 @@ jobs:
echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
- name: Generate release notes - name: Generate release notes
run: | run: |
@ -447,7 +477,7 @@ jobs:
done done
- name: Create Release - name: Create Release
uses: softprops/action-gh-release@v2 uses: softprops/action-gh-release@v3
with: with:
tag_name: ${{ steps.vars.outputs.tag }} tag_name: ${{ steps.vars.outputs.tag }}
name: ${{ steps.vars.outputs.release_name }} name: ${{ steps.vars.outputs.release_name }}

View File

@ -22,6 +22,15 @@
- Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`). - Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`).
- **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority). - **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority).
## GraalVM native image (desktop)
- Enabled via `graalvm { }` inside `nucleus.application` in `composeApp/build.gradle.kts`. Nucleus downloads GraalVM CE itself (or uses `$GRAALVM_HOME`). Linux needs `gcc` + `patchelf`.
- Build: `:composeApp:createGraalvmNativeDistributable` (output `composeApp/build/compose/binaries/main/graalvm-app/`), installers via `packageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}`. A build takes ~10 min and uses all cores and ~15 GB RAM.
- Hand-maintained + agent-recorded metadata lives in `composeApp/graalvm/reachability-metadata.json`. After adding features that use reflection/JNI/JNA (vlcj, Zipline, plugins), run `:composeApp:runWithNativeAgent`, exercise the feature, then close the window normally (the merge only happens on a clean exit).
- `uk.co.caprica.vlcj.binding.lib.LibVlc` must stay registered (`allDeclaredMethods` + `jniAccessible`): JNA direct mapping finds its `native` methods by reflection.
- The `*UberJarForCurrentOS` task excludes other OSes'/CPUs' native libs and stale CEF binaries, because native-image embeds every registered resource into the binary. On Linux, `copyGraalvmJvmSo` is disabled and `copyGraalvmAwtSoLibs` prefers native-image's own libs (10 KB `libjvm.so`/`libjava.so` shims instead of the JDK's 28 MB `libjvm.so`).
- `isProductionBuild` (top of `composeApp/build.gradle.kts`) is true for `package*`/`create*`/`notarize*`/`nativeImageCompile` tasks, or with `-Pproduction=true|false`. It gates the Rust release profile (Gobley `jvmVariant`) so `:composeApp:run` keeps fast debug Rust builds. Put new slow, ship-only optimizations behind it.
- Full user-facing build docs are in `README.md` ("Building from source").
## Codegen and plugin packaging ## Codegen and plugin packaging
- JS plugin bundles: `:js_plugin_example:packageDevelopmentPlugin` and `:js_plugin_example:packageProductionPlugin`. Output is `.smplug` files in `js_plugin_example/build/distributions/`. - JS plugin bundles: `:js_plugin_example:packageDevelopmentPlugin` and `:js_plugin_example:packageProductionPlugin`. Output is `.smplug` files in `js_plugin_example/build/distributions/`.
- Zipline plugin entrypoint: `mainFunction = "org.spotube.js_plugin_example.main"` in `js_plugin_example/build.gradle.kts`. Plugin metadata from `js_plugin_example/plugin.json`. - Zipline plugin entrypoint: `mainFunction = "org.spotube.js_plugin_example.main"` in `js_plugin_example/build.gradle.kts`. Plugin metadata from `js_plugin_example/plugin.json`.
@ -53,7 +62,7 @@
- `prepare-deps` job checks out & publishes to `mavenLocal`: - `prepare-deps` job checks out & publishes to `mavenLocal`:
- `kdroidFilter/ComposeNativeWebview` (compose-webview) - `kdroidFilter/ComposeNativeWebview` (compose-webview)
- `team-spotube/gradle-plugin` (spotubeGradle + vlcjBundler plugins) - `team-spotube/gradle-plugin` (spotubeGradle + vlcjBundler plugins)
- Build jobs per platform (Android, Linux, Windows, macOS), all `needs: prepare-deps`. - Build jobs per platform (Android, Linux, Windows, macOS), all `needs: prepare-deps`. Desktop jobs ship GraalVM native installers (`packageGraalvm{Deb,Rpm,Exe,Dmg}`, output in `composeApp/build/compose/binaries/main/graalvm-<format>/`), not jpackage/JVM ones.
- **Stable**: reads `versionName` from `composeApp/build.gradle.kts`, tag = `v{version}`, draft release. - **Stable**: reads `versionName` from `composeApp/build.gradle.kts`, tag = `v{version}`, draft release.
- **Nightly**: builds with `-PversionName=nightly`, tag = `nightly` (updates existing), prerelease. - **Nightly**: builds with `-PversionName=nightly`, tag = `nightly` (updates existing), prerelease.
- **Android signing**: decodes `secrets.KEYSTORE` (base64) → `composeApp/upload-keystore.jks`, writes signing config into `local.properties` from secrets. - **Android signing**: decodes `secrets.KEYSTORE` (base64) → `composeApp/upload-keystore.jks`, writes signing config into `local.properties` from secrets.

View File

@ -47,7 +47,104 @@ Grab the latest nightly builds of Spotube [from the Offical Website](https://spo
<a href="https://github.com/team-spotube/spotube/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?+label=Build%20Status"></a> <a href="https://github.com/team-spotube/spotube/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?+label=Build%20Status"></a>
You can compile Spotube's source code by [following these instructions](CONTRIBUTION.md#your-first-code-contribution). Spotube is a Kotlin Multiplatform + Compose Multiplatform app. The desktop app is built with the [Nucleus](https://nucleusframework.dev) Gradle plugin and released as a **GraalVM native image**: an ahead-of-time compiled binary that needs no JVM installed.
### Prerequisites
- **JDK 21** to run Gradle. The Gradle daemon JDK and the GraalVM toolchain are provisioned automatically.
- **Rust** (`rustup` + `cargo`). The `composeApp` Rust library is built through [Gobley](https://gobley.dev).
- **Android SDK**, only for Android builds.
- **Unpublished dependencies.** Three dependencies aren't on Maven Central yet. Clone each one and run `./gradlew publishToMavenLocal` in it:
- [kdroidFilter/ComposeNativeWebview](https://github.com/kdroidFilter/ComposeNativeWebview), at the commit pinned in [`spotube-release-binary.yml`](.github/workflows/spotube-release-binary.yml) (`COMPOSE_WEBVIEW_COMMIT`)
- [team-spotube/gradle-plugin](https://github.com/team-spotube/gradle-plugin)
- [team-spotube/vlcj-bundler-gradle-plugin](https://github.com/team-spotube/vlcj-bundler-gradle-plugin)
- **Linux runtime:** `webkit2gtk-4.1` for the embedded webview.
Always use the Gradle wrapper (`./gradlew`, or `gradlew.bat` on Windows).
### Development
```bash
./gradlew :composeApp:run # desktop (JVM), fastest edit/run loop
./gradlew :composeApp:assembleStableDebug # Android debug APK (or assembleNightlyDebug)
```
Development builds stay fast. The release-only optimizations (the Rust release profile with LTO and symbol stripping, the native image, native-library pruning) are only applied when you build something to ship. See [Production vs development builds](#production-vs-development-builds).
### Building the desktop app (GraalVM native image)
#### Native toolchain
[Nucleus downloads and caches GraalVM CE](https://nucleusframework.dev/en/docs/performance/graalvm/) under `~/.gradle/nucleus/graalvm` on the first native build. Set `GRAALVM_HOME` to use your own GraalVM instead. Each OS also needs a native C toolchain:
| OS | Requirements |
|---|---|
| Linux | `gcc`, `zlib` headers, `patchelf`; plus `rpm` for `.rpm` packages. Debian/Ubuntu: `sudo apt-get install build-essential zlib1g-dev patchelf rpm`. Fedora: `sudo dnf install gcc zlib-devel patchelf rpm-build` |
| macOS | Xcode Command Line Tools (`xcode-select --install`) |
| Windows | Visual Studio Build Tools with the "Desktop development with C++" workload. Build from an "x64 Native Tools" prompt if `native-image` can't find MSVC. |
A native build is a whole-program compile. It takes **about 10 minutes**, uses **every CPU core**, and peaks at roughly **15 GB of RAM** (it gets slower, not broken, with less). That only happens for the native tasks below, never for `run`.
#### Build and run
```bash
# App folder: binary + its native libs, in composeApp/build/compose/binaries/main/graalvm-app/
./gradlew :composeApp:createGraalvmNativeDistributable
# Run that folder's binary directly
./composeApp/build/compose/binaries/main/graalvm-app/org.spotube.spotube/spotube
# Or build and launch in one step
./gradlew :composeApp:runGraalvmNativeDistributable
```
Keep the `spotube` binary next to the `.so`/`.dll`/`lib/` files it was built with; it loads them from its own folder.
#### Installers
Each OS can only build its own installers:
| OS | Command | Output |
|---|---|---|
| Linux | `./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm` (also `packageGraalvmAppImage`) | `composeApp/build/compose/binaries/main/graalvm-{deb,rpm}/` |
| Windows | `./gradlew :composeApp:packageGraalvmExe` (or `packageGraalvmMsi`) | `composeApp/build/compose/binaries/main/graalvm-exe/` |
| macOS | `./gradlew :composeApp:packageGraalvmDmg` | `composeApp/build/compose/binaries/main/graalvm-dmg/` |
Add `-Pnightly=true` for the nightly channel (separate icons and name).
#### Reflection metadata (when a feature breaks only in the native build)
A native image is a closed world: anything the app reaches through reflection, JNI or JNA (vlcj, the Zipline/QuickJS plugin engine, NewPipe, ktor, datastore, …) must be declared at build time. Nucleus generates most of this automatically. The rest is in [`composeApp/graalvm/reachability-metadata.json`](composeApp/graalvm/reachability-metadata.json).
If something works in `:composeApp:run` but throws `ClassNotFoundException`, `NoSuchMethodException`, `MissingReflectionRegistrationError` or similar in the native binary, record what's missing:
```bash
./gradlew :composeApp:runWithNativeAgent
```
The app starts on the JVM with GraalVM's tracing agent attached. Use the broken feature (and anything near it), then **close the window normally**: the recorded entries are merged into `composeApp/graalvm/` only on a clean exit, not with Ctrl+C. Commit the updated JSON and rebuild the native image.
`./gradlew :composeApp:runGraalvmNative` gives a faster debugging loop: a quick-build (`-Ob`) native image that reports exactly which registration is missing.
Keep the hand-written `uk.co.caprica.vlcj.binding.lib.LibVlc` entry. JNA's direct mapping looks up its `native` methods through reflection, and without the entry, playback fails to initialize.
### Production vs development builds
Production mode is switched on automatically when you ask Gradle for a shippable artifact: any `package*`, `create*`, `notarize*` or `nativeImageCompile` task. You can force it either way with `-Pproduction=true` or `-Pproduction=false`.
| | Development (`run`, debug builds) | Production (installers, native image) |
|---|---|---|
| Rust library (desktop) | `debug` profile, fast incremental compile | `release` profile with LTO, single codegen unit, stripped (31 MB → 1.8 MB) |
| Desktop app | JVM, hot-reloadable | GraalVM native image, `-Os` (size-optimized) |
| Native libraries | everything on the classpath | only the host OS/CPU's natives; other OSes' and CPUs' libraries are pruned from the uber JAR the image is compiled from |
| Skia (Skiko) | extracted from the JAR | loaded from `lib/` next to the binary, not embedded a second time |
| JDK runtime libs (Linux) | n/a | native-image's ~10 KB `libjvm.so`/`libjava.so` shims instead of the JDK's (28 MB `libjvm.so`) |
Android picks the Rust profile from its build type (`debug` or `release`) on its own.
### Releases
Releases are built by [`spotube-release-binary.yml`](.github/workflows/spotube-release-binary.yml), run manually (`workflow_dispatch`) as `stable` or `nightly`. Each desktop platform (Linux x86_64 and aarch64, Windows, macOS) builds its native installers on its own runner. Android builds a signed APK.
## 👥 The Spotube team ## 👥 The Spotube team

View File

@ -14,4 +14,9 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
[lib] [lib]
crate-type = ["cdylib", "staticlib"] crate-type = ["cdylib", "staticlib"]
path = "src/commonMain/rust/lib.rs" path = "src/commonMain/rust/lib.rs"
[profile.release]
strip = true
lto = true
codegen-units = 1

View File

@ -16,7 +16,9 @@
*/ */
import gobley.gradle.GobleyHost import gobley.gradle.GobleyHost
import gobley.gradle.Variant
import gobley.gradle.cargo.dsl.jvm import gobley.gradle.cargo.dsl.jvm
import dev.nucleusframework.desktop.application.dsl.NativeImageOptimization
import dev.nucleusframework.desktop.application.dsl.TargetFormat import dev.nucleusframework.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.reload.gradle.ComposeHotRun import org.jetbrains.compose.reload.gradle.ComposeHotRun
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@ -50,6 +52,17 @@ val desktopIconDir = if (isNightly) "icons/nightly" else "icons/stable"
val nightlyBuildNumber = providers.gradleProperty("buildNumber").orNull?.toIntOrNull() val nightlyBuildNumber = providers.gradleProperty("buildNumber").orNull?.toIntOrNull()
?: System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull() ?: System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull()
?: buildNumber ?: buildNumber
// Production = shipping artifacts (installers, distributables, native images). Size/speed
// optimizations that slow builds down (Rust release + LTO) only apply then, never to `run`/dev.
// Override with -Pproduction=true|false.
val isProductionBuild = providers.gradleProperty("production").map { it.toBoolean() }.getOrElse(
gradle.startParameter.taskNames.any { task ->
task.substringAfterLast(':').let { name ->
name.startsWith("package") || name.startsWith("create") ||
name.startsWith("notarize") || name == "nativeImageCompile"
}
}
)
val targetName = "Spotube" val targetName = "Spotube"
val homePageUrl = "https://spotube.org" val homePageUrl = "https://spotube.org"
val repositoryUrl = "https://github.com/team-spotube/spotube" val repositoryUrl = "https://github.com/team-spotube/spotube"
@ -431,6 +444,84 @@ nucleus.application {
buildTypes.release.proguard { buildTypes.release.proguard {
configurationFiles.from(project.file("proguard-rules.pro")) configurationFiles.from(project.file("proguard-rules.pro"))
} }
// AOT-compiled native binary (no bundled JVM). Build with `packageGraalvmNative`, try it with
// `runGraalvmNative`. Nucleus downloads GraalVM CE on first use (or uses $GRAALVM_HOME).
// Reflection/JNI metadata the automatic passes miss (vlcj, Zipline, NewPipe/Rhino, uniffi)
// lives in `graalvm/` and is (re)collected by exercising the app under `runWithNativeAgent`.
graalvm {
isEnabled = true
imageName = "spotube"
nativeImageConfigBaseDir.set(project.layout.projectDirectory.dir("graalvm"))
// ProGuard is not used for native images; -Os is the size equivalent
optimization = NativeImageOptimization.SIZE
// Same ceiling as the JVM distribution's -Xmx
maxHeapSize = "384m"
// Skiko is loaded from lib/ next to the binary (Nucleus copies it there); don't embed
// a second ~30 MB copy as a resource
buildArgs.add("-H:+UnlockExperimentalVMOptions")
buildArgs.add("-H:ExcludeResources=.*skiko-[^/]*\\.(so|dylib|dll)")
}
}
// The uber JAR is what the GraalVM build compiles from, and native-image embeds every registered
// resource in the binary. Libraries ship natives for every OS/CPU, so keep only the host's
// (the uber JAR is per-OS/arch already; it isn't used by `run`).
tasks.withType<org.gradle.jvm.tasks.Jar>().matching { it.name.endsWith("UberJarForCurrentOS") }.configureEach {
// Everything the exclude closure uses is local: the configuration cache can't serialize
// references to the build script object.
val hostOs = System.getProperty("os.name").lowercase()
val foreignExtensions = when {
hostOs.contains("win") -> setOf("so", "dylib", "jnilib", "a")
hostOs.contains("mac") -> setOf("so", "dll", "a")
else -> setOf("dll", "dylib", "jnilib", "a")
}
val nativeExtensions = setOf("so", "dll", "dylib", "jnilib", "a")
// CPU names as libraries spell them in their native folders (linux-x86-64, jni/amd64, darwin-aarch64, …)
val cpuDirToken = Regex(
"(?:^|[-_])(x86[-_]64|amd64|x64|aarch64|arm64|armv6|armv7|armel|arm|x86|i[36]86|ppc64le|ppc64|ppc" +
"|s390x|riscv64|loongarch64|mips64el|sparcv9|sparc)(?=$|[-_])"
)
val hostCpuTokens = when (System.getProperty("os.arch").lowercase()) {
"aarch64", "arm64" -> setOf("aarch64", "arm64")
else -> setOf("x86-64", "x86_64", "amd64", "x64")
}
// OS flavours we never ship desktop builds for (sqlite-jdbc and JNA bundle these)
val foreignOsDirToken = Regex("android|musl|freebsd|openbsd|dragonflybsd|aix|sunos")
exclude { element ->
val segments = element.path.lowercase().split('/')
val extension = segments.last().substringAfterLast('.', "")
!element.isDirectory && extension in nativeExtensions && (
extension in foreignExtensions ||
segments.dropLast(1).any { dir ->
foreignOsDirToken.containsMatchIn(dir) ||
cpuDirToken.findAll(dir).any { it.groupValues[1] !in hostCpuTokens }
}
)
}
// The locally built compose-webview SNAPSHOT carries stale CEF binaries (~1.3 GB unstripped
// libcef.so) that no code loads (Linux uses WebKitGTK).
exclude(
"nucleus/native/*/libcef.*",
"nucleus/native/*/libcompose_cef_*",
"nucleus/native/*/cef_subprocess*",
"nucleus/native/*/*.pak",
"nucleus/native/*/icudtl.dat",
"nucleus/native/*/v8_context_snapshot.bin",
)
}
// Linux: native-image emits 13 KB libjvm.so/libjava.so shims (the image itself provides the JVM
// symbols), but Nucleus copies the JDK's real ones next to the binary — libjvm.so alone is 28 MB.
// Ship native-image's own outputs instead: skip the libjvm copy, and in the AWT copy prefer
// nativeCompile's version of any library native-image produced over the JDK's.
tasks.matching { it.name == "copyGraalvmJvmSo" }.configureEach { enabled = false }
tasks.withType<Copy>().matching { it.name == "copyGraalvmAwtSoLibs" }.configureEach {
val nativeCompileDir = layout.buildDirectory.dir("compose/tmp/main/graalvm/nativeCompile").get().asFile
from(nativeCompileDir) { include("*.so") }
eachFile {
if (file.parentFile != nativeCompileDir && nativeCompileDir.resolve(name).exists()) exclude()
}
} }
// Issue with compose plugin // Issue with compose plugin
@ -446,6 +537,8 @@ tasks.withType<ComposeHotRun>().configureEach {
} }
cargo { cargo {
// Gobley defaults desktop to the debug profile, which ships an unstripped ~31 MB library
if (isProductionBuild) jvmVariant = Variant.Release
builds.jvm { builds.jvm {
embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget) embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget)
} }

File diff suppressed because it is too large Load Diff