diff --git a/.github/workflows/freebsd.yml b/.github/workflows/freebsd.yml new file mode 100644 index 0000000..8974daf --- /dev/null +++ b/.github/workflows/freebsd.yml @@ -0,0 +1,50 @@ +# Builds and tests the workspace on FreeBSD, inside a FreeBSD VM on a Linux runner +# (GitHub has no FreeBSD runners). GPU tests skip themselves when the VM has no adapter, and the +# player falls back to its silent clock when the VM has no sound device. +name: FreeBSD + +on: + push: + branches: [main] + pull_request: + paths: + - Cargo.toml + - Cargo.lock + - "**/Cargo.toml" + - "crates/playback/**" + - .github/workflows/freebsd.yml + workflow_dispatch: + +concurrency: + group: freebsd-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + freebsd: + name: FreeBSD 14 (build + test) + runs-on: ubuntu-latest + timeout-minutes: 300 + steps: + - uses: actions/checkout@v4 + - name: Build and test in a FreeBSD VM + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1 + with: + release: "14.3" + usesh: true + cpu: 4 + mem: 12288 + # Nothing to bring back; copying the target dir out of the VM fails on large builds. + copyback: false + prepare: | + pkg install -y git curl pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib + curl -fsSL https://sh.rustup.rs | sh -s -- -y --profile minimal + run: | + . "$HOME/.cargo/env" + # The checkout is synced in under another owner; let tests that call git read it. + git config --global --add safe.directory "*" + rustc --version + CARGO_PROFILE_DEV_DEBUG=0 cargo build --workspace --all-targets --locked + CARGO_PROFILE_DEV_DEBUG=0 cargo test --workspace --locked diff --git a/.github/workflows/packaging-lint.yml b/.github/workflows/packaging-lint.yml new file mode 100644 index 0000000..88694ec --- /dev/null +++ b/.github/workflows/packaging-lint.yml @@ -0,0 +1,57 @@ +# Quick static checks for the release pipeline (seconds, no Rust build): workflow syntax, +# packaging shell/PowerShell scripts, and the Linux desktop/AppStream/Flatpak metadata. +name: Packaging lint + +on: + push: + branches: [main, release] + paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"] + pull_request: + paths: [".github/workflows/**", "packaging/**", "assets/app-icon/**"] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: packaging-lint-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + env: + ACTIONLINT_VERSION: 1.7.12 + steps: + - uses: actions/checkout@v4 + - name: Install tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends shellcheck desktop-file-utils appstream libxml2-utils python3-yaml + curl -fsSL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | tar -xz -C "$RUNNER_TEMP" actionlint + - name: actionlint + run: | + "$RUNNER_TEMP/actionlint" -color + - name: shellcheck + run: shellcheck packaging/*.sh packaging/*/*.sh + - name: PowerShell + shell: pwsh + run: | + foreach ($f in Get-ChildItem packaging/windows/*.ps1) { + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errors) | Out-Null + if ($errors) { $errors | ForEach-Object { Write-Output "::error file=$($f.Name)::$_" }; exit 1 } + Write-Output "ok $($f.Name)" + } + - name: XML, desktop entry, AppStream, Flatpak manifest + run: | + xmllint --noout packaging/windows/soundcraft.wxs packaging/macos/*.plist* packaging/linux/*.xml* + # Windows Installer needs Icon ids ending in .ico or .exe, or shortcuts show a blank icon. + if grep -oE '"$RUNNER_TEMP/ai.storyteller.soundcraft.metainfo.xml" + appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.soundcraft.metainfo.xml" + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.soundcraft.yml')); assert m['id'] == 'ai.storyteller.soundcraft' and m['command'] == 'soundcraft'" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..29907ae --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,333 @@ +# Release pipeline: every push to `release` builds signed installers for every platform and +# creates or updates the draft GitHub Release "SoundCraft v". See docs/releasing.md. +# +# All jobs run in the `release` environment (restricted to the `release` branch), which holds +# the signing secrets. Every secret is optional: missing ones produce unsigned artifacts and a +# warning, never a failed build. +name: Release + +on: + push: + branches: [release] + workflow_dispatch: + inputs: + version: + description: "Version override for a test run, e.g. 0.2.0-rc.1 (empty: Cargo.toml)" + type: string + default: "" + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + # Never cancel a run mid-notarization or mid-upload; a newer push waits its turn. + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 + SOUNDCRAFT_BUILD_SHA: ${{ github.sha }} + INPUT_VERSION: ${{ inputs.version }} + +jobs: + version: + name: version + runs-on: ubuntu-latest + environment: release + outputs: + version: ${{ steps.v.outputs.version }} + date: ${{ steps.v.outputs.date }} + prerelease: ${{ steps.v.outputs.prerelease }} + steps: + - uses: actions/checkout@v4 + - id: v + name: Resolve version + run: | + . packaging/env.sh + v="${INPUT_VERSION#v}" + v="${v:-$VERSION}" + if ! printf '%s' "$v" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then + echo "::error::'$v' is not a version like 1.2.3 or 1.2.3-rc.1" + exit 1 + fi + case "$v" in *-*) pre=true ;; *) pre=false ;; esac + { + echo "version=$v" + echo "date=$(date -u +%Y-%m-%d)" + echo "prerelease=$pre" + } >>"$GITHUB_OUTPUT" + echo "### SoundCraft v$v (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY" + + macos: + name: macOS universal + needs: version + runs-on: macos-15 + environment: release + env: + SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }} + SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: aarch64-apple-darwin, x86_64-apple-darwin + - uses: Swatinem/rust-cache@v2 + with: + key: release-macos-universal + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$SOUNDCRAFT_VERSION" + - name: Import signing certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: packaging/macos/import-cert.sh + - name: Build, sign, notarize + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: packaging/macos/package.sh --arch universal + - uses: actions/upload-artifact@v4 + with: + name: release-macos + path: dist/release/* + if-no-files-found: error + - name: Remove temporary keychain + if: always() + run: | + if [ -n "${MACOS_KEYCHAIN:-}" ]; then security delete-keychain "$MACOS_KEYCHAIN" || true; fi + + windows: + name: Windows ${{ matrix.arch }} + needs: version + runs-on: windows-latest + environment: release + strategy: + fail-fast: false + matrix: + include: + - { arch: x64, target: x86_64-pc-windows-msvc } + - { arch: x86, target: i686-pc-windows-msvc } + # Windows on ARM: cross-compiled on the x64 runner, so signing and WiX run as above. + # .github/workflows/windows-arm64.yml installs and runs it on ARM64 hardware. + - { arch: arm64, target: aarch64-pc-windows-msvc } + env: + SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }} + SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - uses: Swatinem/rust-cache@v2 + with: + key: release-windows-${{ matrix.arch }} + - name: Apply version override + if: inputs.version != '' + shell: bash + run: cargo xtask version set "$SOUNDCRAFT_VERSION" + - name: Install WiX + shell: pwsh + run: dotnet tool install --global wix --version 5.0.2 + - name: Build, sign, package + shell: pwsh + env: + WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }} + WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_SIGNING_ENDPOINT: ${{ secrets.AZURE_SIGNING_ENDPOINT }} + AZURE_SIGNING_ACCOUNT: ${{ secrets.AZURE_SIGNING_ACCOUNT }} + AZURE_CERT_PROFILE: ${{ secrets.AZURE_CERT_PROFILE }} + run: ./packaging/windows/package.ps1 -Arch ${{ matrix.arch }} + - uses: actions/upload-artifact@v4 + with: + name: release-windows-${{ matrix.arch }} + path: dist/release/* + if-no-files-found: error + + linux: + name: Linux ${{ matrix.arch }} + needs: version + # The oldest GitHub-hosted image, so the binaries only need glibc >= 2.35 (Ubuntu 22.04+, + # Debian 12+, Fedora 36+, RHEL 10). See docs/releasing.md. + runs-on: ${{ matrix.runner }} + environment: release + strategy: + fail-fast: false + matrix: + include: + - { arch: x86_64, runner: ubuntu-22.04, deb: amd64 } + - { arch: aarch64, runner: ubuntu-22.04-arm, deb: arm64 } + env: + SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }} + SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + NFPM_VERSION: 2.47.0 + steps: + - uses: actions/checkout@v4 + - name: Install build and packaging tools + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev libgl1-mesa-dev \ + libasound2-dev pkg-config \ + desktop-file-utils appstream python3-yaml + curl -fsSL -o "$RUNNER_TEMP/nfpm.deb" \ + "https://github.com/goreleaser/nfpm/releases/download/v${NFPM_VERSION}/nfpm_${NFPM_VERSION}_${{ matrix.deb }}.deb" + sudo dpkg -i "$RUNNER_TEMP/nfpm.deb" + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: release-linux-${{ matrix.arch }} + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$SOUNDCRAFT_VERSION" + - name: Build and package (AppImage, deb, rpm, tar.gz) + run: packaging/linux/package.sh + - name: Check the packages + run: | + dpkg-deb --info dist/release/*.deb + dpkg-deb --contents dist/release/*.deb | grep -E 'usr/bin/soundcraft$|metainfo|applications' + ldd target/release/soundcraft + # cpal's ALSA backend: the one audio library linked at build time (see nfpm.yaml). + ldd target/release/soundcraft | grep -q libasound + python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.soundcraft.yml')); assert m['id'] == 'ai.storyteller.soundcraft'" + for appimage in dist/release/*.AppImage; do + chmod +x "$appimage" + APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version + done + - uses: actions/upload-artifact@v4 + with: + name: release-linux-${{ matrix.arch }} + path: dist/release/* + if-no-files-found: error + + freebsd: + name: FreeBSD x86_64 + needs: version + # GitHub has no FreeBSD runners: build in a FreeBSD 14.3 VM (same image as freebsd.yml). + runs-on: ubuntu-latest + timeout-minutes: 150 + environment: release + env: + SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }} + SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + if: inputs.version != '' + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$SOUNDCRAFT_VERSION" + - name: Build and package in a FreeBSD VM + # v1.5.9, pinned by commit. + uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc + with: + release: "14.3" + usesh: true + mem: 12288 + # Passed through by name; never templated into the script. + envs: "SOUNDCRAFT_VERSION SOUNDCRAFT_BUILD_DATE SOUNDCRAFT_BUILD_SHA" + # alsa-lib: cpal's audio backend on FreeBSD (ALSA over OSS via alsa-plugins-oss at runtime). + prepare: | + pkg install -y bash curl git pkgconf cmake python3 libxkbcommon wayland libX11 libXcursor libXrandr libXi libxcb mesa-libs vulkan-loader gtk3 fontconfig freetype2 alsa-lib + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain stable + run: | + . "$HOME/.cargo/env" + git config --global --add safe.directory "$PWD" + # Build outside the checkout so only dist/ is copied back out of the VM. + export CARGO_TARGET_DIR=/var/tmp/soundcraft-target CARGO_BUILD_JOBS=4 + bash packaging/freebsd/package.sh + - uses: actions/upload-artifact@v4 + with: + name: release-freebsd + path: dist/release/* + if-no-files-found: error + + web: + name: Web (wasm) + needs: version + runs-on: ubuntu-latest + environment: release + env: + SOUNDCRAFT_VERSION: ${{ needs.version.outputs.version }} + SOUNDCRAFT_BUILD_DATE: ${{ needs.version.outputs.date }} + TRUNK_VERSION: 0.21.14 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + - uses: Swatinem/rust-cache@v2 + with: + key: release-web + - name: Install trunk + run: | + curl -fsSL "https://github.com/trunk-rs/trunk/releases/download/v${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz" \ + | tar -xz -C "$HOME/.cargo/bin" trunk + trunk --version + - name: Apply version override + if: inputs.version != '' + run: cargo xtask version set "$SOUNDCRAFT_VERSION" + - name: Build and zip + run: packaging/web/package.sh + - uses: actions/upload-artifact@v4 + with: + name: release-web + path: dist/release/* + if-no-files-found: error + + release: + name: Draft GitHub Release + needs: [version, macos, windows, linux, freebsd, web] + runs-on: ubuntu-latest + environment: release + permissions: + contents: write + env: + VERSION: ${{ needs.version.outputs.version }} + PRERELEASE: ${{ needs.version.outputs.prerelease }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + steps: + - uses: actions/download-artifact@v4 + with: + pattern: release-* + path: artifacts + merge-multiple: true + - name: Checksums + working-directory: artifacts + run: | + sha256sum -- * | tee SHA256SUMS.txt + - name: Create or update the draft release + working-directory: artifacts + run: | + tag="v$VERSION" + title="SoundCraft v$VERSION" + pre=() + if [ "$PRERELEASE" = true ]; then pre=(--prerelease); fi + if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then + if [ "$draft" != "true" ]; then + echo "::error::Release $tag is already published. Bump the version (cargo xtask version set) before pushing to release." + exit 1 + fi + echo "Updating draft $tag: replacing its assets" + gh release view "$tag" --json assets --jq '.assets[].name' | while read -r asset; do + gh release delete-asset "$tag" "$asset" --yes + done + gh release edit "$tag" --draft --title "$title" --target "$GITHUB_SHA" --prerelease="$PRERELEASE" + gh release upload "$tag" --clobber -- * + else + echo "Creating draft $tag" + gh release create "$tag" --draft --title "$title" --target "$GITHUB_SHA" --generate-notes "${pre[@]}" -- * + fi + { + echo "Draft release **$title** at \`${GITHUB_SHA::9}\`:" + echo + sed 's/^/ /' SHA256SUMS.txt + } >>"$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/windows-arm64.yml b/.github/workflows/windows-arm64.yml new file mode 100644 index 0000000..79f0e93 --- /dev/null +++ b/.github/workflows/windows-arm64.yml @@ -0,0 +1,95 @@ +# Windows on ARM: package the ARM64 release the way release.yml does (cross-compiled on an x64 +# runner, unsigned here), then install that MSI on a Windows 11 ARM64 runner, check the installed +# binaries are ARM64, run the CLI natively, and uninstall. (A native ARM64 `cargo test` job is left +# out for now: PrintCraft saw an intermittent access violation in egui UI tests there.) +name: Windows ARM64 + +on: + push: + branches: [main] + pull_request: + paths: + - Cargo.toml + - Cargo.lock + - "**/Cargo.toml" + - "**/build.rs" + - .github/workflows/windows-arm64.yml + - packaging/windows/** + schedule: + - cron: "41 4 * * 1" # weekly, so drift in the runner images shows up + workflow_dispatch: + +concurrency: + group: windows-arm64-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +jobs: + package: + name: package (cross-compiled on x64) + runs-on: windows-latest + timeout-minutes: 90 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@89b12181fb390509a0842a86cc55eeb8eb928c1d # stable + with: + targets: aarch64-pc-windows-msvc + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + key: windows-arm64-package + - name: Install WiX + shell: pwsh + run: dotnet tool install --global wix --version 5.0.2 + - name: Build and package (unsigned) + shell: pwsh + env: + DIST: ${{ github.workspace }}\dist\arm64 + run: ./packaging/windows/package.ps1 -Arch arm64 + - uses: actions/upload-artifact@v4 + with: + name: soundcraft-windows-arm64 + path: dist/arm64/* + if-no-files-found: error + retention-days: 7 + + install: + name: install and run on ARM64 + needs: package + runs-on: windows-11-arm + timeout-minutes: 30 + steps: + - uses: actions/download-artifact@v4 + with: + name: soundcraft-windows-arm64 + path: dist + - name: Install the MSI, check it, run it, uninstall it + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $msi = (Get-ChildItem dist/*.msi | Select-Object -First 1).FullName + $p = Start-Process msiexec.exe -ArgumentList '/i', "`"$msi`"", '/qn', '/l*v', 'install.log' -Wait -PassThru + if ($p.ExitCode -ne 0) { Get-Content install.log -Tail 60; throw "msiexec /i exited $($p.ExitCode)" } + $dir = Join-Path $env:ProgramFiles 'SoundCraft' + foreach ($exe in 'soundcraft.exe', 'soundcraft-cli.exe') { + $path = Join-Path $dir $exe + if (-not (Test-Path $path)) { throw "$exe not installed in $dir" } + $bytes = [System.IO.File]::ReadAllBytes($path) + $machine = [BitConverter]::ToUInt16($bytes, [BitConverter]::ToInt32($bytes, 0x3C) + 4) + if ($machine -ne 0xAA64) { throw "$exe is for machine 0x$('{0:X}' -f $machine), not ARM64" } + Write-Output "ok $path (ARM64)" + } + # The installed CLI runs natively on ARM64. + $cli = Join-Path $dir 'soundcraft-cli.exe' + $out = & $cli --version 2>&1 | Out-String + if ($LASTEXITCODE -ne 0) { Write-Output $out; throw "soundcraft-cli --version exited $LASTEXITCODE" } + Write-Output "ok soundcraft-cli --version: $($out.Trim())" + $p = Start-Process msiexec.exe -ArgumentList '/x', "`"$msi`"", '/qn' -Wait -PassThru + if ($p.ExitCode -ne 0) { throw "msiexec /x exited $($p.ExitCode)" } + if (Test-Path (Join-Path $dir 'soundcraft.exe')) { throw 'uninstall left soundcraft.exe behind' } + Write-Output 'ok uninstalled' diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 1f7708e..332c2f9 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -9,8 +9,9 @@ Assets must be original, public domain (CC0) or permissively licensed. See `AGEN ## Icons All UI icons (tools, transport, track buttons) are drawn procedurally in -`crates/ui-egui/src/icons.rs` by the SoundCraft contributors (MIT OR Apache-2.0). No icon image -files are used. +`crates/ui-egui/src/icons.rs` by the SoundCraft contributors (MIT OR Apache-2.0). No UI icon image +files are used. The app icon (`assets/app-icon/`, a singing nightingale drawn as original SVG by +the SoundCraft contributors) is listed under Files below. ## Audio @@ -36,3 +37,9 @@ and, at runtime, the operating system's own UI fonts when available (never redis | `docs/brand/artcraft-mark.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | | `docs/brand/artcraft-mark-black.svg` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | | `docs/brand/artcraft-mark-black.png` | ArtCraft Team | craftrules `assets/brand/` | ArtCraft brand terms | +| `assets/app-icon/icon.svg` | SoundCraft contributors | original (hand-placed SVG paths; placeholder app icon) | MIT OR Apache-2.0 | +| `assets/app-icon/soundcraft-1024.png` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | +| `assets/app-icon/soundcraft-macos-512.png` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | +| `assets/app-icon/soundcraft.icns` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | +| `assets/app-icon/soundcraft.ico` | SoundCraft contributors | original, rendered from `icon.svg` | MIT OR Apache-2.0 | +| `assets/app-icon/hicolor/` | SoundCraft contributors | original, rendered from `icon.svg` (16–512 px PNGs and the scalable SVG) | MIT OR Apache-2.0 | diff --git a/Cargo.lock b/Cargo.lock index f500bd3..60e9409 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3357,6 +3357,7 @@ dependencies = [ "soundcraft-engine", "soundcraft-playback", "soundcraft-ui-egui", + "winresource", ] [[package]] @@ -3435,6 +3436,7 @@ name = "soundcraft-mix" version = "0.1.0" dependencies = [ "log", + "rayon", "soundcraft-audio-io", "soundcraft-dsp", "soundcraft-midi", @@ -3868,10 +3870,12 @@ version = "1.1.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "920602543f0911ab71da12c50d59701da54c196d1a2bf5cb4b75667f137a406a" dependencies = [ + "indexmap", "serde_core", "serde_spanned", "toml_datetime", "toml_parser", + "toml_writer", "winnow", ] @@ -3905,6 +3909,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + [[package]] name = "tracing" version = "0.1.44" @@ -4942,6 +4952,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winresource" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0986a8b1d586b7d3e4fe3d9ea39fb451ae22869dcea4aa109d287a374d866087" +dependencies = [ + "toml", + "version_check", +] + [[package]] name = "wit-bindgen" version = "0.57.1" diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..ccb552b --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,53 @@ +# SoundCraft roadmap + +SoundCraft is a clean-room, pure-Rust digital audio workstation that aims at full parity with +Avid Pro Tools, and then beyond it: faster, open, scriptable and agent-controllable. This file is +the honest status: what works today, what is missing, and how far we are. + +## Where we are (2026-10-07) + +| Measure | Value | How | +|---|---|---| +| Menu-catalog parity (engine + UI) | see [`docs/parity.md`](docs/parity.md) | `cargo xtask parity` compares the incumbent's 512 menu leaves (names observed black-box) with our command registry | +| Estimated overall feature parity | **~40 %** | judgement across the areas below, weighted by how much professional users rely on them | +| Estimated remaining effort | **~220–300 wall-clock hours** of Claude Opus 5.5 work (single agent; roughly 70–100 hours with 3–4 agents in parallel) | sum of the per-area estimates below | + +### Area by area + +| Area | Status | Parity | Remaining (h) | +|---|---|---:|---:| +| Session model, save/load, undo | Done: tracks, playlists, clips, fades, clip gain, automation, markers, groups, busses; JSON `.scraft` + Audio Files folder; COW undo with gesture coalescing | 80 % | 10 | +| Audio file I/O | WAV/BWF/RF64, AIFF/AIFC, FLAC (read/write); MP3, OGG, AAC, ALAC, CAF (read); peaks | 75 % | 10 | +| Editing | Slip/Shuffle/Spot/Grid, cut/copy/paste/clear/duplicate/repeat/shift, separate/heal/trim/consolidate/strip silence, fades & crossfades, nudge, playlists, grabber/trim/selector/smart/pencil/zoom/scrubber gestures, edit groups | 60 % | 35 | +| Mixer & routing | Faders, pan, mute/solo (SIP, implicit solo), sends pre/post, busses, aux inputs, master faders, VCAs, routing folders, 10 inserts, automation read | 60 % | 30 | +| Automation | Breakpoint lanes for volume/pan/mute/sends/plugin params; modes stored; write/thin/glide/convert commands | 45 % | 25 | +| Plugins (built-in) | 26 original processors + 2 instruments, AudioSuite offline processing | 45 % | 30 | +| Third-party plugins (CLAP/VST3/AU) | Not started | 0 % | 40 | +| MIDI | MIDI/instrument tracks, SMF import/export, MIDI editor (piano roll + velocity), event list, step input, quantize/transpose/velocity/duration ops | 40 % | 30 | +| Recording | Audio input capture, punch into selection; loop record, QuickPunch, input monitoring with latency compensation missing | 30 % | 20 | +| Elastic Audio / TCE / Beat Detective | Clip stretch ratio, offline time-stretch and pitch-shift, transient detection | 20 % | 25 | +| Video, surround panning, Atmos | Formats modelled; no video track, no surround panner | 5 % | 40 | +| UI fidelity | Edit + Mix windows, toolbar, rulers, track headers, menus for the whole catalog, floating windows, dialogs | 65 % | 25 | +| Agent control | CLI, JSON control channel, MCP server (headless + bridged), offscreen UI renders | 90 % (ahead of the incumbent) | 5 | +| Release engineering | Signed macOS universal, Windows x64/x86, Linux AppImage/deb/rpm/tar/Flatpak, FreeBSD, Web/WASM on every push to `release` | 80 % | 5 | + +## Current focus + +1. Land the engine parity batch (Edit/View/Track/Clip/Event/Options/Setup menu items). +2. Automation write modes during playback (touch/latch/write from fader moves). +3. Loop record and QuickPunch; input monitoring. +4. Plugin hosting (CLAP first, it is MIT-licensed and Rust-friendly). +5. Elastic Audio in realtime; Beat Detective workflow window. +6. README screenshots and the first alpha release. + +## Milestones + +- **M0–M3 (done):** workspace, gates, model, IO, DSP, mix engine, command engine, Edit/Mix UI. +- **M4 (done):** realtime playback, metering, plugins, bounce. +- **M5 (in progress):** editing depth (tools, modes, playlists, fades) — most items landed. +- **M6 (in progress):** recording. +- **M7 (in progress):** MIDI editing. +- **M8 (done for the first cut):** CLI, control channel, MCP, agent acceptance test. +- **M9:** AudioSuite breadth, Elastic Audio, Beat Detective. +- **M10:** alpha release on all platforms. +- **M11:** third-party plugin hosting, video, surround. diff --git a/apps/soundcraft/Cargo.toml b/apps/soundcraft/Cargo.toml index eb9569f..4e1bc75 100644 --- a/apps/soundcraft/Cargo.toml +++ b/apps/soundcraft/Cargo.toml @@ -21,3 +21,7 @@ log.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] rfd.workspace = true + +[build-dependencies] +# Icon + VERSIONINFO for soundcraft.exe; build.rs only uses it when the target is Windows. +winresource = "0.1" diff --git a/apps/soundcraft/build.rs b/apps/soundcraft/build.rs new file mode 100644 index 0000000..4df35b9 --- /dev/null +++ b/apps/soundcraft/build.rs @@ -0,0 +1,30 @@ +//! Windows only: embed the app icon and version info (VERSIONINFO) into `soundcraft.exe`, so it +//! shows in Explorer, the taskbar, the Start menu and Alt-Tab. +//! +//! On every other target this does nothing. A missing resource compiler is a warning, so a +//! cross-compile from macOS or Linux still links, unless `SOUNDCRAFT_REQUIRE_WINRES=1` turns it +//! into an error (for release builds). + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../../assets/app-icon/soundcraft.ico"); + println!("cargo:rerun-if-env-changed=SOUNDCRAFT_REQUIRE_WINRES"); + if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") { + return; + } + let mut res = winresource::WindowsResource::new(); + res.set_icon("../../assets/app-icon/soundcraft.ico") + .set("ProductName", "SoundCraft") + .set("FileDescription", "SoundCraft digital audio workstation") + .set("CompanyName", "Learning Machines LLC") + .set("LegalCopyright", "Copyright (c) the SoundCraft authors. MIT OR Apache-2.0.") + .set("OriginalFilename", "soundcraft.exe") + .set("InternalName", "soundcraft"); + if let Err(e) = res.compile() { + if std::env::var_os("SOUNDCRAFT_REQUIRE_WINRES").is_some() { + eprintln!("embedding Windows resources failed: {e}"); + std::process::exit(1); + } + println!("cargo:warning=soundcraft.exe built without icon/version resources: {e}"); + } +} diff --git a/apps/soundcraft/src/main.rs b/apps/soundcraft/src/main.rs index 0de0592..0515d0b 100644 --- a/apps/soundcraft/src/main.rs +++ b/apps/soundcraft/src/main.rs @@ -121,6 +121,16 @@ fn services() -> Services { } } +/// The window, Dock, taskbar and Alt-Tab icon. macOS gets Apple's icon grid (transparent margin); +/// elsewhere the full tile. Regenerate with `packaging/icons.sh`. +fn app_icon() -> Option { + #[cfg(target_os = "macos")] + let png: &[u8] = include_bytes!("../../../assets/app-icon/soundcraft-macos-512.png"); + #[cfg(not(target_os = "macos"))] + let png: &[u8] = include_bytes!("../../../assets/app-icon/hicolor/256x256/apps/ai.storyteller.soundcraft.png"); + eframe::icon_data::from_png_bytes(png).map_err(|e| log::warn!("app icon: {e}")).ok() +} + fn main() -> eframe::Result { let args: Vec = std::env::args().skip(1).collect(); if args.iter().any(|a| a == "--version" || a == "-V") { @@ -146,7 +156,7 @@ fn main() -> eframe::Result { .collect(); let engine = if demo { soundcraft_engine::demo::demo_engine() } else { Engine::default() }; - let options = eframe::NativeOptions { + let mut options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("SoundCraft") .with_inner_size([1600.0, 1000.0]) @@ -155,6 +165,9 @@ fn main() -> eframe::Result { .with_app_id("ai.storyteller.soundcraft"), ..Default::default() }; + if let Some(icon) = app_icon() { + options.viewport = options.viewport.with_icon(icon); + } eframe::run_native( "SoundCraft", options, diff --git a/assets/app-icon/README.md b/assets/app-icon/README.md new file mode 100644 index 0000000..12cea9c --- /dev/null +++ b/assets/app-icon/README.md @@ -0,0 +1,44 @@ +# SoundCraft app icon (placeholder) + +**A nightingale singing**, as an engraving-style head-and-shoulders portrait in the Crafting Apps' +owl-template framing (craftrules `standards/icon-design.md`): a full-bleed colour field, the bird +in three-quarter view with its bill open and its eye on the viewer, its breast and wing running off +the bottom and right edges, three song arcs leaving the bill. + +This is a **placeholder** until the owner draws SoundCraft's icon in ArtCraft like the other apps' +icons. Replace `icon.svg` and run `packaging/icons.sh`; nothing else needs to change. + +## Palette + +Exactly three colours: + +| Colour | Hex | Used for | +|---|---|---| +| Ink | `#0b0b0c` | Line work and contour | +| Paper | `#efe9dc` | The bird and the song arcs | +| SoundCraft teal (app colour) | `#14a9c4` | The full-bleed background field | + +## Geometry + +A 512 × 512 viewBox, clipped to a rounded square with `rx=112`. Windows and Linux use the full +tile. The macOS icons (`soundcraft.icns`, `soundcraft-macos-512.png`) sit on Apple's grid: an +824/1024 body with a transparent margin. + +## Provenance and licence + +Drawn from scratch as hand-placed SVG paths by the SoundCraft contributors; no source image was +traced or copied. MIT OR Apache-2.0, like the code (see `ATTRIBUTION.md`). + +## Files + +| File | What it is | +|---|---| +| `icon.svg` | The canonical vector | +| `soundcraft-1024.png` | 1024 px render | +| `soundcraft-macos-512.png` | Runtime window/Dock icon on macOS (Apple grid margin) | +| `soundcraft.icns` | macOS bundle icon | +| `soundcraft.ico` | Windows icon (16–256 px), embedded in `soundcraft.exe` and used by the MSI | +| `hicolor/x/apps/ai.storyteller.soundcraft.png` | Linux/FreeBSD icon theme, 16–512 px, plus `scalable/` | + +Regenerate everything with `packaging/icons.sh` (needs `resvg`, and `iconutil` on macOS for the +`.icns`). diff --git a/assets/app-icon/hicolor/128x128/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..17d2540 Binary files /dev/null and b/assets/app-icon/hicolor/128x128/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/16x16/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..2be8640 Binary files /dev/null and b/assets/app-icon/hicolor/16x16/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/24x24/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..13a89fb Binary files /dev/null and b/assets/app-icon/hicolor/24x24/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/256x256/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..080df61 Binary files /dev/null and b/assets/app-icon/hicolor/256x256/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/32x32/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..e6448b8 Binary files /dev/null and b/assets/app-icon/hicolor/32x32/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/48x48/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..d3be101 Binary files /dev/null and b/assets/app-icon/hicolor/48x48/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/512x512/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..2ec6bf9 Binary files /dev/null and b/assets/app-icon/hicolor/512x512/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png new file mode 100644 index 0000000..8029709 Binary files /dev/null and b/assets/app-icon/hicolor/64x64/apps/ai.storyteller.soundcraft.png differ diff --git a/assets/app-icon/hicolor/scalable/apps/ai.storyteller.soundcraft.svg b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.soundcraft.svg new file mode 100644 index 0000000..2828853 --- /dev/null +++ b/assets/app-icon/hicolor/scalable/apps/ai.storyteller.soundcraft.svg @@ -0,0 +1 @@ +SoundCraft diff --git a/assets/app-icon/icon.svg b/assets/app-icon/icon.svg new file mode 100644 index 0000000..2828853 --- /dev/null +++ b/assets/app-icon/icon.svg @@ -0,0 +1 @@ +SoundCraft diff --git a/assets/app-icon/soundcraft-1024.png b/assets/app-icon/soundcraft-1024.png new file mode 100644 index 0000000..1bef16b Binary files /dev/null and b/assets/app-icon/soundcraft-1024.png differ diff --git a/assets/app-icon/soundcraft-macos-512.png b/assets/app-icon/soundcraft-macos-512.png new file mode 100644 index 0000000..b360ca8 Binary files /dev/null and b/assets/app-icon/soundcraft-macos-512.png differ diff --git a/assets/app-icon/soundcraft.icns b/assets/app-icon/soundcraft.icns new file mode 100644 index 0000000..8c1dd3d Binary files /dev/null and b/assets/app-icon/soundcraft.icns differ diff --git a/assets/app-icon/soundcraft.ico b/assets/app-icon/soundcraft.ico new file mode 100644 index 0000000..c9afa38 Binary files /dev/null and b/assets/app-icon/soundcraft.ico differ diff --git a/crates/playback/src/record.rs b/crates/playback/src/record.rs index edeb4fb..13befaa 100644 --- a/crates/playback/src/record.rs +++ b/crates/playback/src/record.rs @@ -28,8 +28,9 @@ pub struct Recorder { pub channels: usize, } -/// Cap: one hour of 8-channel 192 kHz audio. -const MAX_SAMPLES: usize = 3600 * 192_000 * 8; +/// Cap: one hour of 8-channel 192 kHz audio; 64 Mi samples (256 MB of f32) on 32-bit targets +/// (wasm32, i686), where the 64-bit product doesn't fit in `usize`. +const MAX_SAMPLES: usize = if usize::BITS >= 64 { (3600u64 * 192_000 * 8) as usize } else { 64 << 20 }; impl Recorder { /// Open the default input device. Errors when there is none (the caller shows a message). diff --git a/docs/release-playbook.md b/docs/release-playbook.md new file mode 100644 index 0000000..aafba97 --- /dev/null +++ b/docs/release-playbook.md @@ -0,0 +1,9 @@ +# Release playbook + +The release playbook shared by every crafting app lives in craftrules: +[`../craftrules/release/playbook.md`](../../craftrules/release/playbook.md) +(`storytold/craftrules`). It is the canonical copy; don't duplicate it here. + +SoundCraft's implementation (`.github/workflows/release.yml`, `packaging/`, +`cargo xtask version`) was copied from DesignCraft/PhotoCraft and renamed. SoundCraft-specific +details are in [releasing.md](releasing.md). diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 0000000..1d89fcd --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,201 @@ +# Releasing SoundCraft + +Every push to the `release` branch runs `.github/workflows/release.yml`. The workflow builds +signed installers for macOS, Windows, Linux, FreeBSD and the web, then creates or updates a +**draft** GitHub Release named `SoundCraft v`. Nobody sees a draft until a maintainer +publishes it. + +This is SoundCraft's implementation of the shared [release playbook](release-playbook.md). +User-facing names say **SoundCraft**. Files, binaries and ids stay lowercase +(`soundcraft---.`, `ai.storyteller.soundcraft`). + +## Cutting a release + +1. **Bump the version** on `main`. The only place it lives is `[workspace.package] version` in + the root `Cargo.toml`; the app reads it from `CARGO_PKG_VERSION` (*About SoundCraft*, + `soundcraft --version`, `soundcraft-cli --version`). + + ```sh + cargo xtask version # prints the current version, e.g. 0.1.0 + cargo xtask version set 0.2.0 # or 0.2.0-rc.1; updates Cargo.toml and Cargo.lock + ``` + + Commit the change (`Cargo.toml` + `Cargo.lock`) through the normal review flow. +2. **Merge `main` into `release`** (or fast-forward it) and push. The workflow starts by itself. +3. **Wait for the draft.** After about 30 to 60 minutes (notarization and the FreeBSD VM are the + slow parts), the Releases page has a draft `SoundCraft v0.2.0`, tagged `v0.2.0` on the pushed + commit, with every artifact and `SHA256SUMS.txt`. The notes are generated from the merged PRs. +4. **Check it.** Download an installer or two and look at the job summaries. Any `::warning::` + there means a signing secret was missing and that artifact is unsigned. +5. **Publish** the draft in the GitHub UI. Publishing creates the `v0.2.0` tag. Versions with a + pre-release suffix (`-rc.1`) are marked as pre-releases. + +Pushing to `release` again before you publish rebuilds the same draft and replaces its assets. +After the draft is published, the workflow refuses to touch that version again, so bump it first. + +**Test runs:** *Actions → Release → Run workflow* runs the whole pipeline by hand. The optional +`version` input (such as `0.2.0-rc.1`) overrides `Cargo.toml` for that run only. The jobs apply it +with `cargo xtask version set` before building, so the binaries report it too. The run still needs +the `release` environment, which only the `release` branch can use, so pick that branch in the +dialog. + +## What gets built + +| Platform | Artifacts | Built on | +|---|---|---| +| macOS 11+ (universal: Apple silicon + Intel) | `soundcraft--macos-universal.dmg`, `soundcraft-cli--macos-universal.zip` | `macos-15` | +| Windows 10+ x64 | `soundcraft--windows-x64.msi`, `soundcraft--windows-x64-portable.zip` | `windows-latest` | +| Windows 10+ x86 (32-bit) | `soundcraft--windows-x86.msi`, `soundcraft--windows-x86-portable.zip` | `windows-latest` | +| Windows 11 ARM64 | `soundcraft--windows-arm64.msi`, `soundcraft--windows-arm64-portable.zip` | `windows-latest` (cross-compiled; `windows-arm64.yml` installs and runs it on ARM64) | +| Linux x86_64 | `soundcraft--linux-x86_64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04` | +| Linux aarch64 | `soundcraft--linux-aarch64.{AppImage,deb,rpm,tar.gz}` | `ubuntu-22.04-arm` | +| FreeBSD 14 x86_64 | `soundcraft--freebsd-x86_64.tar.gz` (a `/usr/local` tree) | FreeBSD 14.3 VM on `ubuntu-latest` | +| Web | `soundcraft-web-.zip` (static site; see [`packaging/web/README.md`](../packaging/web/README.md)) | `ubuntu-latest` | + +The Flatpak manifest (`packaging/linux/flatpak/ai.storyteller.soundcraft.yml`) is kept ready for a +Flathub submission and validated in CI, but not built there (see Linux below). + +### Audio libraries + +SoundCraft plays and records through [cpal](https://github.com/RustAudio/cpal): CoreAudio on +macOS, WASAPI on Windows, ALSA on Linux and FreeBSD (PulseAudio and PipeWire through their ALSA +plugins), and WebAudio in the browser. + +- **Linux** builds need the ALSA headers (`libasound2-dev` and `pkg-config` on Ubuntu). The binary + links `libasound.so.2`, so the `.deb` depends on `libasound2 | libasound2t64` and the `.rpm` on + `alsa-lib`; the AppImage and tarball expect it on the host (every desktop distro has it). +- **FreeBSD** builds need `alsa-lib` (`pkg install alsa-lib`); at runtime `alsa-plugins-oss` + routes ALSA to the OSS `sound(4)` devices. Without a device the player falls back to its + silent clock, so the transport still runs. +- **Flatpak**: alsa-lib and its PulseAudio plugin come with the freedesktop runtime; the + manifest grants `--socket=pulseaudio`. +- **Web**: browsers start audio only after a user gesture, so the first click in the page may be + needed before sound plays. The web build has no recording. + +### macOS + +`packaging/macos/package.sh` builds `aarch64-apple-darwin` and `x86_64-apple-darwin` with +`MACOSX_DEPLOYMENT_TARGET=11.0`, joins them with `lipo`, and assembles `SoundCraft.app`: + +- `Info.plist` is generated from `Info.plist.in`. The bundle id is `ai.storyteller.soundcraft`. + It declares the `.scraft` session type (`ai.storyteller.soundcraft.session`, MIME + `application/x-soundcraft-session`, role Editor, rank Owner) and lists WAV, AIFF/AIFC, FLAC, + MP3 and MIDI as Viewer/Alternate, so SoundCraft appears under *Open With* without taking over + the default player. `NSMicrophoneUsageDescription` explains the recording prompt. +- **Signing** goes inside-out with the hardened runtime and a secure timestamp. The executable is + signed first, then the bundle. There's no `--deep` on the final signature. `entitlements.plist` + holds a single exception, `com.apple.security.device.audio-input`, without which the hardened + runtime blocks recording. +- **Notarization:** the app is zipped and sent with `xcrun notarytool submit --wait`, then the + ticket is stapled to the app. The app goes on a DMG (`hdiutil`, with an `Applications` link). + The DMG is signed, notarized and stapled too. +- **CLI:** the universal `soundcraft-cli` is signed with the hardened runtime, zipped and + notarized. + +Locally, without certificates, the script signs ad-hoc (`codesign -s -`) and skips notarization: + +```sh +packaging/macos/package.sh # universal; needs both rustup targets +packaging/macos/package.sh --arch aarch64 # quicker, host-only +open dist/release/soundcraft-*-macos-*.dmg +``` + +### Windows + +`packaging/windows/package.ps1 -Arch x64|x86|arm64` builds with `-C target-feature=+crt-static` +(in `CARGO_TARGET__RUSTFLAGS`, so host build scripts aren't affected), so neither the MSI +nor the portable zip needs the Visual C++ redistributable. + +- `apps/soundcraft/build.rs` embeds the icon (`assets/app-icon/soundcraft.ico`) and VERSIONINFO + with `winresource`, only when targeting Windows. Release packaging sets + `SOUNDCRAFT_REQUIRE_WINRES=1` so a missing resource compiler fails the build. +- The script checks both PE headers: the machine type matches `-Arch`, `soundcraft.exe` is a GUI + app (no console window) and `soundcraft-cli.exe` a console app. +- `soundcraft.wxs` (WiX v5) is a per-machine install into Program Files with an advertised Start + Menu shortcut. SoundCraft becomes the default app for `.scraft` and is listed under *Open with* + for `.wav`, `.aif`, `.aiff`, `.flac`, `.mp3`, `.mid` and `.midi`. It also registers App Paths + (Win+R `soundcraft`). The MSI version is the numeric `X.Y.Z`; same-version upgrades let release + candidates replace each other. The upgrade code `EE53D3E5-D0AE-40F9-808D-9417FDEBF61A` is fixed + forever. +- **Signing:** `packaging/windows/sign.ps1` signs both `.exe` files and then the `.msi` with + `signtool` (SHA-256, RFC 3161 timestamp), using a `.pfx` (`WINDOWS_CERTIFICATE*`) if present, + otherwise Azure Trusted Signing (all six `AZURE_*`), otherwise nothing (a warning). + +Locally on Windows: `dotnet tool install -g wix --version 5.0.2`, then +`pwsh packaging/windows/package.ps1 -Arch x64`. + +### Linux + +`packaging/linux/package.sh` stages one FHS tree and makes every format from it: both binaries, +`ai.storyteller.soundcraft.desktop` (with `MimeType=` for sessions, WAV, AIFF, FLAC, MP3 and +MIDI), hicolor icons from 16 px to 512 px plus a scalable SVG, AppStream metainfo, and a +shared-mime-info file declaring `application/x-soundcraft-session` (`*.scraft`). + +- **AppImage** (maintained `AppImage/appimagetool`, static runtime: no libfuse2 needed). +- **.deb** and **.rpm** by [nfpm](https://nfpm.goreleaser.com/) from one `nfpm.yaml`; the + `postinst.sh` hook refreshes the desktop, MIME and icon caches. +- **.tar.gz** for people who manage their own `/opt` or `~/.local`. +- **Flatpak**: `packaging/linux/flatpak/ai.storyteller.soundcraft.yml` builds from source on the + freedesktop 25.08 runtime with Wayland/X11, `dri`, PulseAudio and `xdg-music`; other files go + through the portals. A real build needs vendored crates (`cargo-sources.json` from + `flatpak-cargo-generator.py`); the commands are in the manifest's header. + +Binaries are built on Ubuntu 22.04 and need **glibc ≥ 2.35**. X11, Wayland, xkbcommon, Vulkan +and EGL are loaded at runtime, so the packages declare them as dependencies or recommendations. + +Locally (on Linux): install nfpm, then `packaging/linux/package.sh` (or `--formats "deb tar"`). + +### FreeBSD + +GitHub has no FreeBSD runners, so the `freebsd` job builds inside a FreeBSD 14.3 VM +(`vmactions/freebsd-vm`, pinned by commit) and runs `packaging/freebsd/package.sh`, which stages +a `/usr/local`-style tree (binaries, desktop entry, MIME type, metainfo, icons, docs) as a +`.tar.gz`. Install with `tar -xzf soundcraft-*-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local`. +`.github/workflows/freebsd.yml` builds and tests the workspace there on every push to `main`. + +### Web + +`packaging/web/package.sh` runs `trunk build --release` in `apps/soundcraft-web` (output in +`dist/web`, `public_url = "./"`) and zips the site with sample `_headers` and `.htaccess` files and +the hosting guide. `apps/soundcraft-web` runs the same `SoundApp` as the desktop app on the +synthesised demo session, rendering with wgpu (WebGPU, or WebGL2 with `?webgl`) and playing +through WebAudio. + +## Secrets + +All secrets live in the repository's **`release` environment** (*Settings → Environments → +release*), restricted to the `release` branch. Every job in `release.yml` declares +`environment: release`. Each secret is optional: if one is missing, that platform's artifacts +are unsigned and the run shows a `::warning::`. Scripts never echo secret values; certificates +are written with `umask 077` and deleted in `always()`/`finally` steps. + +| Secret | Used for | +|---|---| +| `APPLE_CERTIFICATE` | base64 `.p12` with "Developer ID Application: Learning Machines LLC (DJ6XS33FX8)" | +| `APPLE_CERTIFICATE_PASSWORD` | password for that `.p12` | +| `KEYCHAIN_PASSWORD` | password for the temporary CI keychain (random if unset) | +| `APPLE_ID` | Apple ID used by `notarytool` | +| `APPLE_PASSWORD` | app-specific password for that Apple ID | +| `APPLE_TEAM_ID` | `DJ6XS33FX8` | +| `WINDOWS_CERTIFICATE` / `WINDOWS_CERTIFICATE_PASSWORD` | base64 `.pfx` (unused: Windows uses Azure) | +| `AZURE_TENANT_ID`, `AZURE_CLIENT_ID`, `AZURE_CLIENT_SECRET` | service principal for Azure Trusted Signing | +| `AZURE_SIGNING_ENDPOINT`, `AZURE_SIGNING_ACCOUNT`, `AZURE_CERT_PROFILE` | Trusted Signing endpoint, account and profile | + +`GITHUB_TOKEN` creates the release. Only the final job gets `contents: write`. Provisioning the +environment, ruleset and secrets for a new repo: craftrules `release/signing-setup.md`. + +## Icons + +`assets/app-icon/icon.svg` is the canonical icon: a **placeholder** singing nightingale, drawn as +original SVG in the three-colour icon palette (ink, paper, SoundCraft teal `#14a9c4`); see +`assets/app-icon/README.md`. `packaging/icons.sh` regenerates the 1024 px PNG, the macOS 512 px +window icon, the `.icns`, the `.ico` (packed by `cargo xtask ico`) and the hicolor PNGs from it. +It needs `resvg`, plus `iconutil` on macOS. The outputs are committed, so packaging never needs +those tools. `apps/soundcraft/src/main.rs` sets the runtime window/Dock/taskbar icon from them. + +## Checks + +`.github/workflows/packaging-lint.yml` runs in seconds on any change to `packaging/`, the +workflows or the icons: actionlint, shellcheck, a PowerShell parse, xmllint (WiX, plists, +AppStream, MIME), `desktop-file-validate`, `appstreamcli validate`, and a YAML check of the +Flatpak manifest. diff --git a/packaging/.shellcheckrc b/packaging/.shellcheckrc new file mode 100644 index 0000000..36c19d0 --- /dev/null +++ b/packaging/.shellcheckrc @@ -0,0 +1,3 @@ +# shellcheck config for the packaging scripts (CI: .github/workflows/packaging-lint.yml). +source-path=SCRIPTDIR +external-sources=true diff --git a/packaging/env.sh b/packaging/env.sh new file mode 100644 index 0000000..cb4bcdb --- /dev/null +++ b/packaging/env.sh @@ -0,0 +1,59 @@ +# shellcheck shell=bash +# Shared setup for the packaging scripts. Source it: `. "$(dirname "$0")/../env.sh"`. +# +# Exports: +# ROOT workspace root +# VERSION [workspace.package] version from Cargo.toml (override: SOUNDCRAFT_VERSION) +# DIST output directory for release artifacts (default: $ROOT/dist/release) +# SOUNDCRAFT_BUILD_SHA git commit (recorded in the macOS Info.plist and the job summary) +# SOUNDCRAFT_BUILD_DATE UTC build date, YYYY-MM-DD (AppStream release date) +# CARGO_TARGET_DIR cargo's target dir (default: $ROOT/target) + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export ROOT + +# The version lives in exactly one place: `[workspace.package] version` in the root Cargo.toml. +# (`cargo xtask version` prints the same thing; awk avoids compiling xtask here.) +workspace_version() { + awk ' + /^\[/ { in_pkg = ($0 == "[workspace.package]") ; next } + in_pkg && $1 == "version" { gsub(/[" ]/, "", $3); print $3; exit } + ' "$ROOT/Cargo.toml" +} + +VERSION="${SOUNDCRAFT_VERSION:-$(workspace_version)}" +if [ -z "$VERSION" ]; then + echo "error: could not read [workspace.package] version from $ROOT/Cargo.toml" >&2 + exit 1 +fi +export VERSION + +DIST="${DIST:-$ROOT/dist/release}" +mkdir -p "$DIST" +export DIST + +if [ -z "${SOUNDCRAFT_BUILD_SHA:-}" ]; then + SOUNDCRAFT_BUILD_SHA="$(git -C "$ROOT" rev-parse HEAD 2>/dev/null || true)" +fi +export SOUNDCRAFT_BUILD_SHA +export SOUNDCRAFT_BUILD_DATE="${SOUNDCRAFT_BUILD_DATE:-$(date -u +%Y-%m-%d)}" +export CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" + +# Emit a GitHub Actions warning (plain stderr outside Actions). +warn() { + if [ -n "${GITHUB_ACTIONS:-}" ]; then echo "::warning::$*"; else echo "warning: $*" >&2; fi +} + +# Copy licence and readme files that exist into a package directory. +copy_docs() { + local dest="$1" f + for f in README.md LICENSE LICENSE-MIT LICENSE-APACHE NOTICE ATTRIBUTION.md COPYRIGHT; do + if [ -f "$ROOT/$f" ]; then cp "$ROOT/$f" "$dest/"; fi + done +} + + +# Portable SHA-256 of a file (prints just the hash). +sha256() { + if command -v sha256sum >/dev/null; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi +} diff --git a/packaging/freebsd/package.sh b/packaging/freebsd/package.sh new file mode 100755 index 0000000..71af80c --- /dev/null +++ b/packaging/freebsd/package.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Build and package SoundCraft for FreeBSD: +# +# $DIST/soundcraft--freebsd-x86_64.tar.gz a /usr/local-style tree: +# soundcraft--freebsd-x86_64/{bin, share/applications, share/icons, share/mime, +# share/metainfo, share/doc/soundcraft} +# +# Install by copying the tree's contents into /usr/local, e.g. +# tar -xzf soundcraft-*-freebsd-x86_64.tar.gz --strip-components 1 -C /usr/local +# +# Usage: packaging/freebsd/package.sh [--skip-build] +# +# Needs: bash, cargo and the build dependencies listed in .github/workflows/freebsd.yml (alsa-lib for +# cpal's audio I/O; at runtime alsa-plugins-oss routes ALSA to FreeBSD's OSS sound(4)). FreeBSD's +# install(1) has no -D, so directories are created first. +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +LINUX="$ROOT/packaging/linux" +APP_ID=ai.storyteller.soundcraft + +SKIP_BUILD=0 +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=1; shift ;; + -h | --help) sed -n '2,15p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +# FreeBSD calls x86_64 "amd64"; release names use the Rust/Linux spelling. +case "$(uname -m)" in + amd64 | x86_64) ARCH=x86_64 ;; + arm64 | aarch64) ARCH=aarch64 ;; + *) echo "unsupported architecture $(uname -m)" >&2; exit 2 ;; +esac +BASENAME="soundcraft-$VERSION-freebsd-$ARCH" + +echo "==> SoundCraft $VERSION for FreeBSD $ARCH" + +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli) +fi +BIN="$CARGO_TARGET_DIR/release" +WORK="$CARGO_TARGET_DIR/freebsd-package" +STAGE="$WORK/$BASENAME" +rm -rf "$WORK" + +# ---- stage a /usr/local-style tree --------------------------------------------------------------- +mkdir -p "$STAGE/bin" "$STAGE/share/applications" "$STAGE/share/mime/packages" "$STAGE/share/metainfo" \ + "$STAGE/share/icons" "$STAGE/share/doc/soundcraft" +install -m 755 "$BIN/soundcraft" "$BIN/soundcraft-cli" "$STAGE/bin/" +strip "$STAGE/bin/soundcraft" "$STAGE/bin/soundcraft-cli" 2>/dev/null || true +# The desktop entry, MIME type and metainfo are the freedesktop files the Linux packages use. +install -m 644 "$LINUX/$APP_ID.desktop" "$STAGE/share/applications/$APP_ID.desktop" +install -m 644 "$LINUX/$APP_ID.mime.xml" "$STAGE/share/mime/packages/$APP_ID.xml" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SOUNDCRAFT_BUILD_DATE/g" \ + "$LINUX/$APP_ID.metainfo.xml.in" >"$STAGE/share/metainfo/$APP_ID.metainfo.xml" +cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/share/icons/" +copy_docs "$STAGE/share/doc/soundcraft" + +# ---- .tar.gz ------------------------------------------------------------------------------------- +tar -C "$WORK" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME" +echo "wrote $DIST/$BASENAME.tar.gz" + +"$STAGE/bin/soundcraft-cli" --version +echo "==> done" +ls -lh "$DIST" diff --git a/packaging/icons.sh b/packaging/icons.sh new file mode 100755 index 0000000..7308e79 --- /dev/null +++ b/packaging/icons.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Regenerate every app icon from assets/app-icon/icon.svg. +# +# Needs: resvg (brew install resvg / cargo install resvg). On macOS, iconutil also writes the +# .icns. The outputs are committed, so builds and packaging never need these tools. +# +# packaging/icons.sh +set -euo pipefail +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DIR="$ROOT/assets/app-icon" +SVG="$DIR/icon.svg" +ID="ai.storyteller.soundcraft" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +command -v resvg >/dev/null || { echo "error: resvg not found (brew install resvg)" >&2; exit 1; } + +# The artwork is a full-bleed 512 tile (rx=112). Windows and Linux use it as is. macOS icons sit on +# Apple's grid: an 824/1024 body with a transparent margin, so widen the viewBox to 512*1024/824. +MAC="$TMP/mac.svg" +sed 's/viewBox="0 0 512 512"/viewBox="-62.136 -62.136 636.272 636.272"/' "$SVG" >"$MAC" +grep -q 'viewBox="-62.136' "$MAC" || { echo "error: expected viewBox=\"0 0 512 512\" in $SVG" >&2; exit 1; } + +render() { resvg -w "$2" -h "$2" "$1" "$3" /dev/null; then + SET="$TMP/soundcraft.iconset" + mkdir -p "$SET" + for s in 16 32 128 256 512; do + render "$MAC" "$s" "$SET/icon_${s}x${s}.png" + render "$MAC" $((s * 2)) "$SET/icon_${s}x${s}@2x.png" + done + iconutil -c icns -o "$DIR/soundcraft.icns" "$SET" +else + echo "warning: iconutil not found (macOS only); soundcraft.icns not regenerated" >&2 +fi +echo "icons written to $DIR" diff --git a/packaging/linux/ai.storyteller.soundcraft.desktop b/packaging/linux/ai.storyteller.soundcraft.desktop new file mode 100644 index 0000000..4a9165e --- /dev/null +++ b/packaging/linux/ai.storyteller.soundcraft.desktop @@ -0,0 +1,14 @@ +[Desktop Entry] +Type=Application +Name=SoundCraft +GenericName=Digital Audio Workstation +Comment=Record, edit and mix audio and MIDI +Exec=soundcraft %F +TryExec=soundcraft +Icon=ai.storyteller.soundcraft +Terminal=false +StartupNotify=true +StartupWMClass=ai.storyteller.soundcraft +Categories=AudioVideo;Audio;Midi;Mixer;Sequencer;Recorder; +MimeType=application/x-soundcraft-session;audio/x-wav;audio/wav;audio/vnd.wave;audio/x-aiff;audio/aiff;audio/flac;audio/x-flac;audio/mpeg;audio/midi;audio/x-midi; +Keywords=daw;audio;mixer;recording;multitrack;midi;editor;music; diff --git a/packaging/linux/ai.storyteller.soundcraft.metainfo.xml.in b/packaging/linux/ai.storyteller.soundcraft.metainfo.xml.in new file mode 100644 index 0000000..9a4fdb5 --- /dev/null +++ b/packaging/linux/ai.storyteller.soundcraft.metainfo.xml.in @@ -0,0 +1,63 @@ + + + + ai.storyteller.soundcraft + CC0-1.0 + MIT OR Apache-2.0 + SoundCraft + Record, edit and mix audio and MIDI + + Learning Machines LLC + + +

+ SoundCraft is an open-source, native digital audio workstation. Record, edit and mix + multitrack audio and MIDI in Edit and Mix windows that work the way studio engineers expect. +

+
    +
  • Non-destructive clip editing with fades, playlists, grids and markers
  • +
  • A mixer with inserts, sends, buses, automation and metering
  • +
  • Built-in EQ, dynamics, reverb, delay, modulation and instruments
  • +
  • Imports WAV, AIFF, FLAC, MP3 and MIDI; bounces to disk
  • +
  • A headless command line and an MCP server for scripting and agents
  • +
+
+ ai.storyteller.soundcraft.desktop + ai.storyteller.soundcraft + + https://getartcraft.com/apps/soundcraft + https://github.com/storytold/soundcraft/issues + https://github.com/storytold/soundcraft + https://discord.gg/artcraft + + AudioVideo + Audio + + + daw + audio + mixer + recording + midi + + + #8fd9e6 + #0b7385 + + + 760 + + + keyboard + pointing + + + soundcraft + application/x-soundcraft-session + + + + + +
diff --git a/packaging/linux/ai.storyteller.soundcraft.mime.xml b/packaging/linux/ai.storyteller.soundcraft.mime.xml new file mode 100644 index 0000000..901cd3e --- /dev/null +++ b/packaging/linux/ai.storyteller.soundcraft.mime.xml @@ -0,0 +1,12 @@ + + + + + SoundCraft session + + + + + diff --git a/packaging/linux/flatpak/ai.storyteller.soundcraft.yml b/packaging/linux/flatpak/ai.storyteller.soundcraft.yml new file mode 100644 index 0000000..c26df34 --- /dev/null +++ b/packaging/linux/flatpak/ai.storyteller.soundcraft.yml @@ -0,0 +1,64 @@ +# Flatpak manifest for SoundCraft, ready for a Flathub submission. +# +# Flatpak builds run offline, so the crate sources must be vendored first. From the repo root: +# +# curl -fsSLO https://raw.githubusercontent.com/flatpak/flatpak-builder-tools/master/cargo/flatpak-cargo-generator.py +# python3 flatpak-cargo-generator.py Cargo.lock -o packaging/linux/flatpak/cargo-sources.json +# flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.soundcraft.yml +# flatpak run ai.storyteller.soundcraft +# +# For Flathub, replace the `dir` source with a `git` source pinned to a release tag and commit. +# +# Audio: cpal talks to ALSA. alsa-lib (headers in the Sdk, the library in the Platform) and its +# PulseAudio plugin ship with the freedesktop runtime, which routes ALSA through the PulseAudio +# socket (PipeWire's pulse server on modern desktops), so no extra module is needed. +id: ai.storyteller.soundcraft +runtime: org.freedesktop.Platform +runtime-version: '25.08' +sdk: org.freedesktop.Sdk +sdk-extensions: + - org.freedesktop.Sdk.Extension.rust-stable +command: soundcraft +finish-args: + - --share=ipc + - --socket=wayland + - --socket=fallback-x11 + - --device=dri + # Playback and recording (ALSA through the PulseAudio/PipeWire socket). + - --socket=pulseaudio + # Open/Save dialogs use the file-chooser portal (rfd), and drag-and-drop uses the document + # portal, so broad filesystem access isn't needed. Music is granted for sessions and their + # "Audio Files" folders opened by path from the command line. + - --filesystem=xdg-music +build-options: + append-path: /usr/lib/sdk/rust-stable/bin +modules: + - name: soundcraft + buildsystem: simple + build-options: + env: + CARGO_HOME: /run/build/soundcraft/cargo + CARGO_NET_OFFLINE: 'true' + build-commands: + - cargo --offline build --release --locked -p soundcraft -p soundcraft-cli + - install -Dm755 target/release/soundcraft /app/bin/soundcraft + - install -Dm755 target/release/soundcraft-cli /app/bin/soundcraft-cli + - install -Dm644 packaging/linux/ai.storyteller.soundcraft.desktop /app/share/applications/ai.storyteller.soundcraft.desktop + - install -Dm644 packaging/linux/ai.storyteller.soundcraft.mime.xml /app/share/mime/packages/ai.storyteller.soundcraft.xml + - mkdir -p /app/share/icons && cp -R assets/app-icon/hicolor /app/share/icons/ + - >- + version=$(awk '/^\[/{p=($0=="[workspace.package]");next} p&&$1=="version"{gsub(/[" ]/,"",$3);print $3;exit}' Cargo.toml); + install -d /app/share/metainfo; + sed -e "s/@VERSION@/$version/" -e "s/@DATE@/$(date -u +%Y-%m-%d)/" + packaging/linux/ai.storyteller.soundcraft.metainfo.xml.in > /app/share/metainfo/ai.storyteller.soundcraft.metainfo.xml + sources: + - type: dir + path: ../../.. + skip: + - target + - dist + - corpus + - plan + - build-flatpak + - .flatpak-builder + - cargo-sources.json diff --git a/packaging/linux/nfpm.yaml b/packaging/linux/nfpm.yaml new file mode 100644 index 0000000..c9f2eb7 --- /dev/null +++ b/packaging/linux/nfpm.yaml @@ -0,0 +1,75 @@ +# .deb and .rpm packages (nfpm: https://nfpm.goreleaser.com). Run by package.sh, which stages +# the file tree, substitutes its path for ${STAGE} (nfpm doesn't expand env vars in `src`) and +# exports VERSION and NFPM_ARCH. +# +# nfpm package -f packaging/linux/nfpm.yaml -p deb -t dist/release/ +name: soundcraft +arch: ${NFPM_ARCH} +platform: linux +version: ${VERSION} +# semver: 0.2.0-rc.1 becomes 0.2.0~rc.1 in deb/rpm, so pre-releases sort before the release. +version_schema: semver +release: 1 +section: sound +priority: optional +maintainer: ${SOUNDCRAFT_MAINTAINER} +vendor: Learning Machines LLC +homepage: https://getartcraft.com/apps/soundcraft +license: MIT OR Apache-2.0 +description: |- + Open-source, native digital audio workstation. Record, edit and mix + multitrack audio and MIDI in Edit and Mix windows; built-in EQ, dynamics, + reverb, delay and instruments; imports WAV, AIFF, FLAC, MP3 and MIDI. + Includes soundcraft-cli, a headless editor, bounce tool and MCP server. + +# The app links glibc, libgcc_s and ALSA (libasound, for audio I/O through cpal; PulseAudio and +# PipeWire are reached through their ALSA plugins). Windowing (X11/Wayland, xkbcommon) and the GPU +# (Vulkan, EGL/GL) are loaded at runtime, so they are dependencies/recommendations here rather +# than ELF NEEDED entries. +overrides: + deb: + depends: + - libc6 (>= 2.35) + - libgcc-s1 + - libasound2 | libasound2t64 + - libxkbcommon0 + - libvulkan1 | libegl1 + recommends: + - libwayland-client0 + - libx11-6 + - libx11-xcb1 + - libxcursor1 + - libxi6 + - libxrandr2 + - mesa-vulkan-drivers + - xdg-desktop-portal + - pipewire-alsa | pulseaudio + rpm: + depends: + - alsa-lib + - libxkbcommon + - (vulkan-loader or libglvnd-egl) + recommends: + - libwayland-client + - libX11 + - libXcursor + - libXi + - libXrandr + - mesa-vulkan-drivers + - xdg-desktop-portal + - (pipewire-alsa or alsa-plugins-pulseaudio) + +contents: + - src: ${STAGE}/usr/bin/soundcraft + dst: /usr/bin/soundcraft + file_info: { mode: 0755 } + - src: ${STAGE}/usr/bin/soundcraft-cli + dst: /usr/bin/soundcraft-cli + file_info: { mode: 0755 } + - src: ${STAGE}/usr/share/ + dst: /usr/share/ + type: tree + +scripts: + postinstall: packaging/linux/postinst.sh + postremove: packaging/linux/postinst.sh diff --git a/packaging/linux/package.sh b/packaging/linux/package.sh new file mode 100755 index 0000000..ba0b6c7 --- /dev/null +++ b/packaging/linux/package.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# Build and package SoundCraft for Linux ( is x86_64 or aarch64): +# +# $DIST/soundcraft--linux-.AppImage any distro with glibc >= the build host's +# $DIST/soundcraft--linux-.deb Debian, Ubuntu, Mint, Pop!_OS, ... +# $DIST/soundcraft--linux-.rpm Fedora, openSUSE, RHEL, ... +# $DIST/soundcraft--linux-.tar.gz plain FHS-style tree (bin/, share/) +# +# Usage: packaging/linux/package.sh [--skip-build] [--formats "appimage deb rpm tar"] +# +# Needs: cargo; nfpm for deb/rpm (https://nfpm.goreleaser.com); appimagetool for the AppImage +# (downloaded into $CARGO_TARGET_DIR if missing). Build on an old distro (CI: Ubuntu 22.04, +# glibc 2.35) so the binaries run on newer ones. Optional: desktop-file-validate, appstreamcli. +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/linux" +APP_ID=ai.storyteller.soundcraft + +SKIP_BUILD=0 +FORMATS="appimage deb rpm tar" +while [ $# -gt 0 ]; do + case "$1" in + --skip-build) SKIP_BUILD=1; shift ;; + --formats) FORMATS="$2"; shift 2 ;; + -h | --help) sed -n '2,15p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done + +ARCH="$(uname -m)" +case "$ARCH" in + x86_64) DEB_ARCH=amd64 ;; + aarch64 | arm64) ARCH=aarch64; DEB_ARCH=arm64 ;; + *) echo "unsupported architecture $ARCH" >&2; exit 2 ;; +esac +export SOUNDCRAFT_MAINTAINER="${SOUNDCRAFT_MAINTAINER:-SoundCraft maintainers }" +BASENAME="soundcraft-$VERSION-linux-$ARCH" + +echo "==> SoundCraft $VERSION for Linux $ARCH ($FORMATS)" + +if [ "$SKIP_BUILD" = 0 ]; then + (cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli) +fi +BIN="$CARGO_TARGET_DIR/release" +WORK="$CARGO_TARGET_DIR/linux-package" +STAGE="$WORK/root" +rm -rf "$WORK" + +# ---- stage an FHS tree (shared by every format) ------------------------------------------------- +install -Dm755 "$BIN/soundcraft" "$STAGE/usr/bin/soundcraft" +install -Dm755 "$BIN/soundcraft-cli" "$STAGE/usr/bin/soundcraft-cli" +strip "$STAGE/usr/bin/soundcraft" "$STAGE/usr/bin/soundcraft-cli" 2>/dev/null || true +install -Dm644 "$HERE/$APP_ID.desktop" "$STAGE/usr/share/applications/$APP_ID.desktop" +install -Dm644 "$HERE/$APP_ID.mime.xml" "$STAGE/usr/share/mime/packages/$APP_ID.xml" +mkdir -p "$STAGE/usr/share/metainfo" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@DATE@/$SOUNDCRAFT_BUILD_DATE/g" \ + "$HERE/$APP_ID.metainfo.xml.in" >"$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" +mkdir -p "$STAGE/usr/share/icons" +cp -R "$ROOT/assets/app-icon/hicolor" "$STAGE/usr/share/icons/" +mkdir -p "$STAGE/usr/share/doc/soundcraft" +copy_docs "$STAGE/usr/share/doc/soundcraft" + +if command -v desktop-file-validate >/dev/null; then + desktop-file-validate "$STAGE/usr/share/applications/$APP_ID.desktop" +fi +if command -v appstreamcli >/dev/null; then + appstreamcli validate --no-net --explain "$STAGE/usr/share/metainfo/$APP_ID.metainfo.xml" +fi + +has() { case " $FORMATS " in *" $1 "*) return 0 ;; *) return 1 ;; esac; } + +# ---- .tar.gz ------------------------------------------------------------------------------------ +if has tar; then + mkdir -p "$WORK/tar" + cp -R "$STAGE/usr" "$WORK/tar/$BASENAME" + tar -C "$WORK/tar" -czf "$DIST/$BASENAME.tar.gz" "$BASENAME" + echo "wrote $DIST/$BASENAME.tar.gz" +fi + +# ---- .deb / .rpm -------------------------------------------------------------------------------- +if has deb || has rpm; then + command -v nfpm >/dev/null || { echo "error: nfpm not found (https://nfpm.goreleaser.com/install/)" >&2; exit 1; } + export VERSION + export NFPM_ARCH="$DEB_ARCH" + # nfpm expands env vars in fields like `version` and `arch`, but not in `contents[].src`. + sed "s|\${STAGE}|$STAGE|g" "$HERE/nfpm.yaml" >"$WORK/nfpm.yaml" + for fmt in deb rpm; do + if has "$fmt"; then (cd "$ROOT" && nfpm package -f "$WORK/nfpm.yaml" -p "$fmt" -t "$DIST/$BASENAME.$fmt"); fi + done +fi + +# ---- AppImage ----------------------------------------------------------------------------------- +if has appimage; then + APPDIR="$WORK/SoundCraft.AppDir" + cp -R "$STAGE" "$APPDIR" + mv "$APPDIR/usr/share/doc" "$WORK/doc-unused" + ln -s usr/bin/soundcraft "$APPDIR/AppRun" + cp "$HERE/$APP_ID.desktop" "$APPDIR/$APP_ID.desktop" + cp "$ROOT/assets/app-icon/hicolor/256x256/apps/$APP_ID.png" "$APPDIR/$APP_ID.png" + ln -s "$APP_ID.png" "$APPDIR/.DirIcon" + + TOOL="${APPIMAGETOOL:-$(command -v appimagetool || true)}" + if [ -z "$TOOL" ]; then + TOOL="$CARGO_TARGET_DIR/appimagetool-$ARCH.AppImage" + if [ ! -x "$TOOL" ]; then + curl -fsSL -o "$TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-$ARCH.AppImage" + chmod +x "$TOOL" + fi + fi + OUT="$DIST/$BASENAME.AppImage" + # Extract-and-run: works without FUSE (containers, CI). The output embeds the static runtime, + # so users don't need libfuse2 either. + ARCH="$ARCH" APPIMAGE_EXTRACT_AND_RUN=1 "$TOOL" --no-appstream "$APPDIR" "$OUT" + echo "wrote $OUT" +fi + +"$STAGE/usr/bin/soundcraft-cli" --version +echo "==> done" +ls -lh "$DIST" diff --git a/packaging/linux/postinst.sh b/packaging/linux/postinst.sh new file mode 100755 index 0000000..775e4df --- /dev/null +++ b/packaging/linux/postinst.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# deb/rpm post-install and post-remove: refresh the desktop, MIME and icon caches. +# Every tool is optional; missing ones are skipped. +if command -v update-desktop-database >/dev/null 2>&1; then + update-desktop-database -q /usr/share/applications || true +fi +if command -v update-mime-database >/dev/null 2>&1; then + update-mime-database /usr/share/mime || true +fi +if command -v gtk-update-icon-cache >/dev/null 2>&1; then + gtk-update-icon-cache -q -t -f /usr/share/icons/hicolor || true +fi +exit 0 diff --git a/packaging/macos/Info.plist.in b/packaging/macos/Info.plist.in new file mode 100644 index 0000000..1b23ea7 --- /dev/null +++ b/packaging/macos/Info.plist.in @@ -0,0 +1,120 @@ + + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + SoundCraft + CFBundleName + SoundCraft + CFBundleExecutable + SoundCraft + CFBundleIdentifier + ai.storyteller.soundcraft + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + @SHORT_VERSION@ + CFBundleVersion + @SHORT_VERSION@ + SoundCraftVersion + @VERSION@ + SoundCraftBuildCommit + @BUILD_SHA@ + CFBundleIconFile + SoundCraft + LSApplicationCategoryType + public.app-category.music + + LSMinimumSystemVersion + 11.0 + NSHighResolutionCapable + + NSSupportsAutomaticGraphicsSwitching + + NSRequiresAquaSystemAppearance + + + NSMicrophoneUsageDescription + SoundCraft records audio from the input you choose. + NSHumanReadableCopyright + Copyright © the SoundCraft authors. MIT OR Apache-2.0. + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + ai.storyteller.soundcraft.session + UTTypeDescription + SoundCraft Session + UTTypeConformsTo + + public.data + public.content + + UTTypeIconFile + SoundCraft + UTTypeTagSpecification + + public.filename-extension + scraft + public.mime-type + application/x-soundcraft-session + + + + + CFBundleDocumentTypes + + + CFBundleTypeName + SoundCraft Session + CFBundleTypeRole + Editor + LSHandlerRank + Owner + CFBundleTypeIconFile + SoundCraft + LSItemContentTypes + ai.storyteller.soundcraft.session + + + + CFBundleTypeName + Audio File + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + + com.microsoft.waveform-audio + public.aiff-audio + public.aifc-audio + org.xiph.flac + public.mp3 + + + + CFBundleTypeName + MIDI File + CFBundleTypeRole + Viewer + LSHandlerRank + Alternate + LSItemContentTypes + public.midi-audio + + + + diff --git a/packaging/macos/entitlements.plist b/packaging/macos/entitlements.plist new file mode 100644 index 0000000..fad948c --- /dev/null +++ b/packaging/macos/entitlements.plist @@ -0,0 +1,13 @@ + + + + + + + com.apple.security.device.audio-input + + + diff --git a/packaging/macos/import-cert.sh b/packaging/macos/import-cert.sh new file mode 100755 index 0000000..17a1a7f --- /dev/null +++ b/packaging/macos/import-cert.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# CI only: import the Developer ID certificate into a temporary keychain so codesign can use it. +# +# Inputs (env): APPLE_CERTIFICATE (base64 .p12), APPLE_CERTIFICATE_PASSWORD, KEYCHAIN_PASSWORD +# (optional; random if unset), RUNNER_TEMP (or TMPDIR). +# Outputs: MACOS_SIGN_IDENTITY (SHA-1 of the identity) and MACOS_KEYCHAIN, appended to +# $GITHUB_ENV when running in Actions, otherwise printed. +# +# Missing certificate: prints a warning and exits 0, so package.sh falls back to ad-hoc signing. +set -euo pipefail +umask 077 +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" + +if [ -z "${APPLE_CERTIFICATE:-}" ]; then + warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized" + exit 0 +fi + +TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}" +KEYCHAIN="$TMP/soundcraft-signing.keychain-db" +CERT="$TMP/soundcraft-signing.p12" +KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}" + +trap 'rm -f "$CERT"' EXIT +printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT" +security create-keychain -p "$KC_PASS" "$KEYCHAIN" +security set-keychain-settings -lut 21600 "$KEYCHAIN" +security unlock-keychain -p "$KC_PASS" "$KEYCHAIN" +security import "$CERT" -P "${APPLE_CERTIFICATE_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security -t cert -f pkcs12 -k "$KEYCHAIN" +rm -f "$CERT" +# Let codesign use the key without a UI prompt. +security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KEYCHAIN" >/dev/null +# Put it on the search list (keeping the login keychain) so codesign and notarytool find it. +# shellcheck disable=SC2046 +security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + +IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')" +if [ -z "$IDENTITY" ]; then + security find-identity -v -p codesigning "$KEYCHAIN" >&2 + echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2 + exit 1 +fi +echo "Imported signing identity $IDENTITY" +if [ -n "${GITHUB_ENV:-}" ]; then + { echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV" +else + echo "MACOS_SIGN_IDENTITY=$IDENTITY" + echo "MACOS_KEYCHAIN=$KEYCHAIN" +fi diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh new file mode 100755 index 0000000..60b7ac0 --- /dev/null +++ b/packaging/macos/package.sh @@ -0,0 +1,162 @@ +#!/usr/bin/env bash +# Build, sign and (optionally) notarize the macOS release artifacts: +# +# $DIST/soundcraft--macos-.dmg SoundCraft.app on a drag-to-Applications DMG +# $DIST/soundcraft-cli--macos-.zip the headless CLI +# +# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build] +# +# Signing (env): +# MACOS_SIGN_IDENTITY codesign identity (name or SHA-1). Default "-" = ad-hoc (local testing; +# Gatekeeper will reject the result on other Macs). In CI, import-cert.sh sets it. +# MACOS_KEYCHAIN keychain holding the identity (optional) +# Notarization (env; all three needed, and a real identity): +# APPLE_ID, APPLE_PASSWORD (app-specific password), APPLE_TEAM_ID +set -euo pipefail +# shellcheck source=../env.sh +. "$(dirname "${BASH_SOURCE[0]}")/../env.sh" +HERE="$ROOT/packaging/macos" + +ARCH=universal +SKIP_BUILD=0 +while [ $# -gt 0 ]; do + case "$1" in + --arch) ARCH="$2"; shift 2 ;; + --skip-build) SKIP_BUILD=1; shift ;; + -h | --help) sed -n '2,16p' "$0"; exit 0 ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done +case "$ARCH" in + universal) TARGETS=(aarch64-apple-darwin x86_64-apple-darwin) ;; + aarch64) TARGETS=(aarch64-apple-darwin) ;; + x86_64) TARGETS=(x86_64-apple-darwin) ;; + *) echo "unknown --arch $ARCH" >&2; exit 2 ;; +esac + +# Keep in sync with LSMinimumSystemVersion in Info.plist.in. +export MACOSX_DEPLOYMENT_TARGET=11.0 +IDENTITY="${MACOS_SIGN_IDENTITY:--}" +SHORT_VERSION="${VERSION%%-*}" +WORK="$CARGO_TARGET_DIR/macos-package" +APP="$WORK/SoundCraft.app" +DMG="$DIST/soundcraft-$VERSION-macos-$ARCH.dmg" +CLI_ZIP="$DIST/soundcraft-cli-$VERSION-macos-$ARCH.zip" + +NOTARIZE=0 +if [ "$IDENTITY" = "-" ]; then + warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); artifacts are not notarized" +elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then + NOTARIZE=1 +else + warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized" +fi + +echo "==> SoundCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE" + +# ---- build ------------------------------------------------------------------------------------- +if [ "$SKIP_BUILD" = 0 ]; then + args=() + for t in "${TARGETS[@]}"; do args+=(--target "$t"); done + (cd "$ROOT" && cargo build --release --locked -p soundcraft -p soundcraft-cli "${args[@]}") +fi + +rm -rf "$WORK" +mkdir -p "$WORK/bin" +for bin in soundcraft soundcraft-cli; do + inputs=() + for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done + lipo -create -output "$WORK/bin/$bin" "${inputs[@]}" + lipo -info "$WORK/bin/$bin" +done + +# ---- signing helpers --------------------------------------------------------------------------- +sign() { + # Hardened runtime + secure timestamp for a real identity; ad-hoc can't be timestamped. + local ts=(--timestamp) + [ "$IDENTITY" = "-" ] && ts=(--timestamp=none) + local kc=() + [ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN") + codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@" +} + +notarize() { + local file="$1" out id status + echo "==> notarizing $(basename "$file") (this can take a few minutes)" + out="$(xcrun notarytool submit "$file" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" --wait --timeout 1h --output-format json)" || true + echo "$out" + id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)" + status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)" + if [ "$status" != "Accepted" ]; then + if [ -n "$id" ]; then + xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true + fi + echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2 + exit 1 + fi +} + +# ---- SoundCraft.app ---------------------------------------------------------------------------- +echo "==> assembling $APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +# Executable and icon carry the display name (CFBundleExecutable / CFBundleIconFile). +cp "$WORK/bin/soundcraft" "$APP/Contents/MacOS/SoundCraft" +cp "$ROOT/assets/app-icon/soundcraft.icns" "$APP/Contents/Resources/SoundCraft.icns" +sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ + -e "s/@BUILD_SHA@/${SOUNDCRAFT_BUILD_SHA:-unknown}/g" \ + "$HERE/Info.plist.in" >"$APP/Contents/Info.plist" +plutil -lint "$APP/Contents/Info.plist" +printf 'APPL????' >"$APP/Contents/PkgInfo" + +# Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). +# Today the only nested code is the main executable; frameworks/helpers would be signed here too. +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/SoundCraft" +sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP" +codesign --verify --strict --deep --verbose=2 "$APP" + +if [ "$NOTARIZE" = 1 ]; then + ditto -c -k --keepParent "$APP" "$WORK/SoundCraft-notarize.zip" + notarize "$WORK/SoundCraft-notarize.zip" + xcrun stapler staple "$APP" + xcrun stapler validate "$APP" + spctl --assess --type execute -vvv "$APP" +fi + +# ---- DMG --------------------------------------------------------------------------------------- +echo "==> building $DMG" +STAGE="$WORK/dmg" +mkdir -p "$STAGE" +ditto "$APP" "$STAGE/SoundCraft.app" +ln -s /Applications "$STAGE/Applications" +rm -f "$DMG" "$WORK/raw.dmg" +# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, +# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. +hdiutil makehybrid -hfs -hfs-volume-name "SoundCraft $VERSION" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" +hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" +rm -f "$WORK/raw.dmg" +sign "$DMG" +codesign --verify --strict --verbose=2 "$DMG" +if [ "$NOTARIZE" = 1 ]; then + notarize "$DMG" + xcrun stapler staple "$DMG" + xcrun stapler validate "$DMG" + spctl --assess --type open --context context:primary-signature -vvv "$DMG" +fi + +# ---- CLI --------------------------------------------------------------------------------------- +echo "==> building $CLI_ZIP" +CLI_DIR="$WORK/soundcraft-cli-$VERSION-macos-$ARCH" +mkdir -p "$CLI_DIR" +cp "$WORK/bin/soundcraft-cli" "$CLI_DIR/" +copy_docs "$CLI_DIR" +sign --options runtime "$CLI_DIR/soundcraft-cli" +codesign --verify --strict --verbose=2 "$CLI_DIR/soundcraft-cli" +rm -f "$CLI_ZIP" +ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP" +# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online. +if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi + +"$WORK/bin/soundcraft-cli" --version +echo "==> done" +ls -lh "$DMG" "$CLI_ZIP" diff --git a/packaging/web/.htaccess b/packaging/web/.htaccess new file mode 100644 index 0000000..7b7e6e1 --- /dev/null +++ b/packaging/web/.htaccess @@ -0,0 +1,16 @@ +# Apache (see HOSTING.md). Needs mod_mime; mod_deflate and mod_headers are optional. +AddType application/wasm .wasm +AddType text/javascript .js + + + AddOutputFilterByType DEFLATE application/wasm text/javascript text/html + + + + + Header set Cache-Control "public, max-age=31536000, immutable" + + + Header set Cache-Control "no-cache" + + diff --git a/packaging/web/README.md b/packaging/web/README.md new file mode 100644 index 0000000..22ab4da --- /dev/null +++ b/packaging/web/README.md @@ -0,0 +1,91 @@ +# Hosting SoundCraft for the web + +`soundcraft-web-.zip` (from the GitHub release, or `packaging/web/package.sh`) holds a +static site in `soundcraft-web-/`: + +| File | What it is | +|---|---| +| `index.html` | The page. It loads everything through relative URLs. | +| `soundcraft-web-.js` | wasm-bindgen glue (generated, ES module) | +| `soundcraft-web-_bg.wasm` | The app (size: see the release; roughly a third of that with compression) | +| `_headers`, `.htaccess` | Sample header rules for Netlify/Cloudflare Pages and Apache | + +There is no server-side code. Upload the folder's contents anywhere that serves static files. + +## Any path works + +All URLs in `index.html` are relative (`public_url = "./"` in `apps/soundcraft-web/Trunk.toml`), +so the site works at a domain root (`https://example.com/`), under a prefix +(`https://example.com/tools/soundcraft/`) and from a CDN bucket. The asset names carry a content +hash, so they can be cached forever. Only `index.html` needs revalidation. + +## Required server settings + +- **MIME type:** serve `.wasm` as `application/wasm`. Browsers refuse to stream-compile it under + any other type, and the app then loads slowly or not at all. Serve `.js` as `text/javascript`. + Most hosts already do both. For nginx, check that `mime.types` has `application/wasm wasm;`. +- **Compression:** turn on gzip or Brotli for `.wasm`, `.js` and `.html`. That takes the + download to roughly a third. You can also precompress (`brotli -k *.wasm`) and let + the server send `Content-Encoding: br`. +- **Caching:** `Cache-Control: public, max-age=31536000, immutable` on the hashed `.wasm` and + `.js` files, and `no-cache` on `index.html`. +- **HTTPS:** WebGPU (and the clipboard) only work in a secure context, which means `https://` + or `http://localhost`. Over plain HTTP elsewhere, the app falls back to WebGL2. +- **No special isolation headers:** SoundCraft plays audio through WebAudio without `SharedArrayBuffer`, so it doesn't + need `Cross-Origin-Opener-Policy` or `Cross-Origin-Embedder-Policy`. If your site already sends + COEP `require-corp`, also send `Cross-Origin-Resource-Policy: same-origin` (or `cross-origin` + when the files live on a CDN) on the app's files. + +nginx example: + +```nginx +location /soundcraft/ { + types { application/wasm wasm; text/javascript js; text/html html; } + gzip on; + gzip_types application/wasm text/javascript text/html; + location ~* \.(wasm|js)$ { add_header Cache-Control "public, max-age=31536000, immutable"; } + location ~* index\.html$ { add_header Cache-Control "no-cache"; } +} +``` + +Local test: `python3 -m http.server 8765` inside the folder, then open http://localhost:8765/. + +## Embedding in a page (iframe) + +```html + +``` + +- The app fills the iframe and follows its size, so size the iframe and not the app. +- Keyboard shortcuts go to the iframe after the user clicks into it, as with any embedded app. +- **Sandboxed iframes** need at least + `sandbox="allow-scripts allow-same-origin"`. +- Don't send `X-Frame-Options: DENY` or a `frame-ancestors` CSP that excludes the embedding page. + +## Renderer selection and fallback flags + +SoundCraft renders with wgpu. It uses **WebGPU** when the browser has it and falls back to +**WebGL2** on its own. URL query flags override this, and they work on the iframe `src` too: + +| Flag | Effect | +|---|---| +| *(none)* | WebGPU if available, otherwise WebGL2 | +| `?webgl` | Force the WebGL2 backend (useful when a WebGPU driver misbehaves) | +| `?empty` | Start with an empty session instead of the synthesised demo session | + +For example: `