From 97d8c23a42868f15a052ec581d232218e9a9b0df Mon Sep 17 00:00:00 2001 From: Seto Date: Sat, 26 Sep 2026 10:48:19 +0200 Subject: [PATCH 1/2] Add Swift SDK release workflow Pushing a swift-vX.Y.Z tag builds DemucsFramework.xcframework with build-swift-xcframework.sh, runs demucs-swift's tests against it, attaches the zip and checksum to a GitHub release, then updates demucs-swift's Package.swift and generated bindings and tags it X.Y.Z. The swift-v prefix keeps these tags out of release.yml's v* trigger. Publishing needs a DEMUCS_SWIFT_PAT secret with write access to SplitFireAI/demucs-swift. The README documents the SDK and the tag. Co-Authored-By: siGit Code Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release-sdk-swift.yml | 189 ++++++++++++++++++++++++ README.md | 19 +++ 2 files changed, 208 insertions(+) create mode 100644 .github/workflows/release-sdk-swift.yml diff --git a/.github/workflows/release-sdk-swift.yml b/.github/workflows/release-sdk-swift.yml new file mode 100644 index 0000000..044e669 --- /dev/null +++ b/.github/workflows/release-sdk-swift.yml @@ -0,0 +1,189 @@ +name: Release SDK Swift + +# Swift SDK releases use their own `swift-vX.Y.Z` tags so they never trigger +# release.yml (CLI, plugin, Pages), whose `v*` pattern does not match them. +on: + push: + tags: + - "swift-v[0-9]+.[0-9]+.[0-9]+" + workflow_dispatch: + inputs: + publish: + description: "Publish demucs-swift release assets from the selected ref" + type: boolean + default: false + release_tag: + description: "Existing swift-vX.Y.Z tag to publish on manual runs" + type: string + required: false + +permissions: + contents: write + +# One release at a time per tag. Not cancel-in-progress: the later steps create +# a GitHub release and then push a commit and a tag to demucs-swift, and killing +# a run part-way through leaves a release with no assets on it, or demucs-swift +# pointing at a release that no longer matches. +concurrency: + group: release-sdk-swift-${{ github.ref }} + cancel-in-progress: false + +jobs: + preflight: + # macOS rather than Linux: the release artifact is an XCFramework and + # demucs-ffi pulls in burn's Metal backend on Apple targets. + runs-on: macos-26 + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + with: + key: sdk-swift-preflight + - name: Validate tag matches demucs-ffi version + if: startsWith(github.ref, 'refs/tags/') + run: | + version="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json,sys; print(next(p["version"] for p in json.load(sys.stdin)["packages"] if p["name"] == "demucs-ffi"))')" + if [ "${GITHUB_REF_NAME#swift-v}" != "$version" ]; then + echo "::error::Tag '$GITHUB_REF_NAME' does not match demucs-ffi version '$version'." + exit 1 + fi + - run: cargo fmt --all -- --check + - run: cargo clippy --locked -p demucs-ffi --all-targets -- -D warnings + - run: cargo test --locked -p demucs-ffi + + build-and-release: + needs: preflight + runs-on: macos-26 + steps: + - uses: actions/checkout@v6 + with: + path: demucs-rs + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-ios,aarch64-apple-ios-sim,aarch64-apple-darwin + # tvOS and visionOS are tier 3: the script builds them with + # `+nightly -Z build-std`, which needs rust-src. + - uses: dtolnay/rust-toolchain@nightly + with: + components: rust-src + - uses: Swatinem/rust-cache@v2 + with: + workspaces: demucs-rs + key: sdk-swift-apple + - name: Build XCFramework + working-directory: demucs-rs + run: .github/scripts/build-swift-xcframework.sh + - id: meta + working-directory: demucs-rs + run: | + echo "version=$(cat dist/swift/version.txt)" >> "$GITHUB_OUTPUT" + echo "checksum=$(cat dist/swift/DemucsFramework.checksum.txt)" >> "$GITHUB_OUTPUT" + - id: release + env: + INPUT_RELEASE_TAG: ${{ inputs.release_tag }} + run: | + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + release_tag="${GITHUB_REF_NAME}" + else + release_tag="${INPUT_RELEASE_TAG}" + fi + echo "release_tag=$release_tag" >> "$GITHUB_OUTPUT" + - name: Validate release tag + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + env: + VERSION: ${{ steps.meta.outputs.version }} + RELEASE_TAG: ${{ steps.release.outputs.release_tag }} + HAS_PAT: ${{ secrets.DEMUCS_SWIFT_PAT != '' }} + run: | + # Fail before the GitHub release exists rather than after, when the + # push to demucs-swift would be the first thing to notice. + if [ "$HAS_PAT" != "true" ]; then + echo "::error::DEMUCS_SWIFT_PAT is not set; it needs contents:write on SplitFireAI/demucs-swift." + exit 1 + fi + if [ -z "$RELEASE_TAG" ]; then + echo "::error::release_tag is required when publish=true on workflow_dispatch runs." + exit 1 + fi + if [ "$RELEASE_TAG" != "swift-v$VERSION" ]; then + echo "::error::Release tag '$RELEASE_TAG' does not match 'swift-v$VERSION' (demucs-ffi version)." + exit 1 + fi + - name: Checkout demucs-swift + uses: actions/checkout@v6 + with: + repository: SplitFireAI/demucs-swift + # Read-only use for dry runs; the PAT is only needed to push, but a + # private repository would need it to clone as well. + token: ${{ secrets.DEMUCS_SWIFT_PAT || github.token }} + path: demucs-swift + # Gate the release on the Swift package building and passing its tests + # against the framework just built. Package.swift prefers a local + # DemucsFramework.xcframework over the release URL when one is present, + # so this exercises the new binary without publishing it first. + - name: Test Swift package against the new framework + working-directory: demucs-swift + run: | + cp -R ../demucs-rs/dist/swift/DemucsFramework.xcframework DemucsFramework.xcframework + cp ../demucs-rs/dist/swift/Package/Sources/Demucs/demucs_ffi.swift Sources/Demucs/demucs_ffi.swift + swift test + rm -rf DemucsFramework.xcframework .build + - name: Upload build artifact + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v7 + with: + name: demucs-swift-xcframework + path: | + demucs-rs/dist/swift/DemucsFramework.xcframework.zip + demucs-rs/dist/swift/DemucsFramework.checksum.txt + demucs-rs/dist/swift/version.txt + demucs-rs/dist/swift/Package/Sources/Demucs/demucs_ffi.swift + - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ steps.release.outputs.release_tag }} + name: Swift SDK ${{ steps.meta.outputs.version }} + generate_release_notes: true + # Keep "Latest" on the CLI/plugin releases from release.yml. + make_latest: false + files: | + demucs-rs/dist/swift/DemucsFramework.xcframework.zip + demucs-rs/dist/swift/DemucsFramework.checksum.txt + - name: Update Swift package + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + env: + CHECKSUM: ${{ steps.meta.outputs.checksum }} + RELEASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ steps.release.outputs.release_tag }}/DemucsFramework.xcframework.zip + run: | + python3 - <<'PY' + import os, pathlib, re + package = pathlib.Path("demucs-swift/Package.swift") + source = package.read_text() + source, urls = re.subn(r'(let releaseFrameworkURL =\s*\n\s*")([^"]+)(")', rf'\g<1>{os.environ["RELEASE_URL"]}\g<3>', source, count=1) + source, sums = re.subn(r'(let releaseFrameworkChecksum =\s*\n\s*")([^"]+)(")', rf'\g<1>{os.environ["CHECKSUM"]}\g<3>', source, count=1) + if (urls, sums) != (1, 1): + raise RuntimeError("Package.swift release constants were not found") + package.write_text(source) + PY + - name: Commit and tag Swift package + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + working-directory: demucs-swift + env: + # SwiftPM resolves `from: "0.1.0"` against plain semver tags, so + # demucs-swift is tagged X.Y.Z rather than swift-vX.Y.Z. + VERSION: ${{ steps.meta.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Package.swift Sources/Demucs/demucs_ffi.swift + git diff --cached --quiet || git commit -m "Release $VERSION" + git push origin HEAD:main + if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q "refs/tags/$VERSION"; then + echo "::warning::Tag $VERSION already exists; leaving it unchanged." + else + git tag -a "$VERSION" -m "Release $VERSION" + git push origin "$VERSION" + fi diff --git a/README.md b/README.md index cc430fd..4cfe447 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,21 @@ The browser version compiles the full inference pipeline to WebAssembly and runs Model weights are downloaded automatically from Hugging Face on first use (both CLI and web). +## Swift SDK + +[demucs-swift](https://github.com/SplitFireAI/demucs-swift) packages the same +inference for iOS, macOS, tvOS, and visionOS apps, GPU-accelerated through +Metal. It is built from the `demucs-ffi` crate (UniFFI) and published by the +`Release SDK Swift` workflow whenever a `swift-vX.Y.Z` tag is pushed; the tag +must match the `demucs-ffi` crate version. + +To build the XCFramework locally (macOS with Xcode, a stable toolchain with the +iOS/macOS targets, and a nightly with `rust-src` for tvOS/visionOS): + +```bash +make swift-xcframework +``` + ## CLI ``` @@ -175,12 +190,14 @@ make web | `make dev` | WASM debug + Vite dev server | | `make dev-release` | WASM release + Vite dev server | | `make web` | Full production web build | +| `make swift-xcframework` | Build the demucs-swift XCFramework and bindings into `dist/swift` (macOS) | | `make clean` | Remove all build artifacts | ### Running Tests ```bash cargo test -p demucs-core +cargo test -p demucs-ffi ``` ## Project Structure @@ -192,6 +209,8 @@ demucs-rs/ ├── demucs-cli/ Native CLI binary (clap, symphonia, indicatif) ├── demucs-plugin/ DAW plugin — VST3/CLAP via nih-plug (macOS, SwiftUI editor) ├── demucs-wasm/ Thin wasm-bindgen adapter over demucs-core +├── demucs-ffi/ UniFFI bridge for the Swift SDK (demucs-swift) +├── uniffi-bindgen/ Generates the Swift bindings from demucs-ffi ├── web/ React + TypeScript frontend (Vite) └── bench/ Python benchmark & validation suite ``` From 8d67f1b3ce32146ef70e34429c421376607a7a80 Mon Sep 17 00:00:00 2001 From: Seto Date: Sat, 26 Sep 2026 12:27:29 +0200 Subject: [PATCH 2/2] Check the demucs-swift tag before creating the release The Swift package commit now happens before the GitHub release, and it fails if demucs-swift already has the version tag with a different Package.swift or bindings. Before, the run warned and pushed to main anyway, leaving SwiftPM users on the old tag. The push rebases onto main first, since main can move while the Swift tests run. The demucs-swift checkout uses full history so the rebase and the tag comparison work. The Package.swift regex no longer requires the string literal on its own line. Co-Authored-By: Claude Opus 5.5 Co-Authored-By: siGit Code --- .github/workflows/release-sdk-swift.yml | 57 +++++++++++++++++-------- 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-sdk-swift.yml b/.github/workflows/release-sdk-swift.yml index 044e669..448c004 100644 --- a/.github/workflows/release-sdk-swift.yml +++ b/.github/workflows/release-sdk-swift.yml @@ -119,6 +119,9 @@ jobs: # private repository would need it to clone as well. token: ${{ secrets.DEMUCS_SWIFT_PAT || github.token }} path: demucs-swift + # Full history and tags: the push step rebases onto main, and the + # commit step compares against an existing release tag. + fetch-depth: 0 # Gate the release on the Swift package building and passing its tests # against the framework just built. Package.swift prefers a local # DemucsFramework.xcframework over the release URL when one is present, @@ -140,18 +143,6 @@ jobs: demucs-rs/dist/swift/DemucsFramework.checksum.txt demucs-rs/dist/swift/version.txt demucs-rs/dist/swift/Package/Sources/Demucs/demucs_ffi.swift - - name: Create GitHub release - if: startsWith(github.ref, 'refs/tags/') || inputs.publish - uses: softprops/action-gh-release@v3 - with: - tag_name: ${{ steps.release.outputs.release_tag }} - name: Swift SDK ${{ steps.meta.outputs.version }} - generate_release_notes: true - # Keep "Latest" on the CLI/plugin releases from release.yml. - make_latest: false - files: | - demucs-rs/dist/swift/DemucsFramework.xcframework.zip - demucs-rs/dist/swift/DemucsFramework.checksum.txt - name: Update Swift package if: startsWith(github.ref, 'refs/tags/') || inputs.publish env: @@ -162,13 +153,15 @@ jobs: import os, pathlib, re package = pathlib.Path("demucs-swift/Package.swift") source = package.read_text() - source, urls = re.subn(r'(let releaseFrameworkURL =\s*\n\s*")([^"]+)(")', rf'\g<1>{os.environ["RELEASE_URL"]}\g<3>', source, count=1) - source, sums = re.subn(r'(let releaseFrameworkChecksum =\s*\n\s*")([^"]+)(")', rf'\g<1>{os.environ["CHECKSUM"]}\g<3>', source, count=1) + source, urls = re.subn(r'(let releaseFrameworkURL =\s*")([^"]+)(")', rf'\g<1>{os.environ["RELEASE_URL"]}\g<3>', source, count=1) + source, sums = re.subn(r'(let releaseFrameworkChecksum =\s*")([^"]+)(")', rf'\g<1>{os.environ["CHECKSUM"]}\g<3>', source, count=1) if (urls, sums) != (1, 1): raise RuntimeError("Package.swift release constants were not found") package.write_text(source) PY - - name: Commit and tag Swift package + # Commit locally before the GitHub release exists, so a conflicting tag + # stops the run while nothing has been published yet. + - name: Commit Swift package if: startsWith(github.ref, 'refs/tags/') || inputs.publish working-directory: demucs-swift env: @@ -180,9 +173,39 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git add Package.swift Sources/Demucs/demucs_ffi.swift git diff --cached --quiet || git commit -m "Release $VERSION" + # A rerun of the same release finds its own tag, which is fine. A tag + # pinning a different framework would leave SwiftPM users on it. + if git rev-parse --verify --quiet "refs/tags/$VERSION^{commit}" > /dev/null; then + if ! git diff --quiet "refs/tags/$VERSION" HEAD -- Package.swift Sources/Demucs/demucs_ffi.swift; then + echo "::error::Tag $VERSION already exists on demucs-swift with a different Package.swift or bindings." + exit 1 + fi + fi + - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ steps.release.outputs.release_tag }} + name: Swift SDK ${{ steps.meta.outputs.version }} + generate_release_notes: true + # Keep "Latest" on the CLI/plugin releases from release.yml. + make_latest: false + files: | + demucs-rs/dist/swift/DemucsFramework.xcframework.zip + demucs-rs/dist/swift/DemucsFramework.checksum.txt + - name: Push and tag Swift package + if: startsWith(github.ref, 'refs/tags/') || inputs.publish + working-directory: demucs-swift + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + # main may have moved while the Swift tests ran. If this fails, the + # release above is already up; rerunning the job replaces its assets + # and retries the push. + git pull --rebase origin main git push origin HEAD:main - if git ls-remote --tags origin "refs/tags/$VERSION" | grep -q "refs/tags/$VERSION"; then - echo "::warning::Tag $VERSION already exists; leaving it unchanged." + if git rev-parse --verify --quiet "refs/tags/$VERSION^{commit}" > /dev/null; then + echo "Tag $VERSION already exists with the same release files." else git tag -a "$VERSION" -m "Release $VERSION" git push origin "$VERSION"