diff --git a/.gitattributes b/.gitattributes index 814775b1c..6ea248b05 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,5 @@ # Preset files are read byte for byte (a saved state is binary): no line-end conversion on checkout. *.ospreset -text + +# Shell scripts run as they are, on Windows too (Git Bash): a CR at each line's end would break them. +*.sh text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6ebec9f58..a23c75732 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,3 +60,20 @@ jobs: - name: Test run: cargo test --workspace --locked + + # What only the release workflow builds: a nightly's and a tagged release's settings still compile. + - name: Check a nightly + run: cargo check --locked -p os-app --bin lemonstudio + env: + LEMON_CHANNEL: nightly + LEMON_VERSION: 2026.1.1.1 + + - name: Check a tagged release + run: cargo check --locked -p os-app --bin lemonstudio + env: + LEMON_CHANNEL: stable + LEMON_VERSION: 0.1.0 + + - name: Test the release scripts + if: runner.os == 'Linux' + run: bash tools/package/test.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..cabe60412 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,290 @@ +# Builds and publishes Lemon Studio's releases, which install and update themselves (docs/releasing.md): +# +# - Nightly: every night from main (or by hand, Run workflow), unless main has not moved since the last nightly. Its +# version is dated, YEAR.MONTH.DAY.N (UTC; N counts that day's nightlies from 1), its release the pre-release +# nightly-. The pre-release "nightly" only holds nightly.json, which points the updater at the newest one; +# the oldest nightlies beyond KEEP_NIGHTLIES are deleted. The schedule only runs with the repository variable +# NIGHTLIES set to "on". +# - Tagged: a pushed tag vMAJOR.MINOR.PATCH, on main and matching the workspace's version in Cargo.toml. Its release +# holds stable.json; GitHub's "latest" release is the newest one. A 0.x version is an alpha. +# +# Run workflow with "dry run" builds and packages everything and signs the manifest, then keeps the lot as the run's +# artifacts instead of publishing it. +# +# Each release has Lemon Studio for Windows (.exe), Linux (.AppImage) and macOS (Apple silicon .app, zipped), each +# carrying its updater, and Lemon Studio's plugins for other DAWs (VST3 and CLAP, a zip per platform). The manifest +# (nightly.json or stable.json) is signed with minisign: the repository secret MINISIGN_SECRET_KEY signs it, and the +# repository variable MINISIGN_PUBLIC_KEY, built into the updater, checks it. +name: Release + +on: + push: + tags: ['v*.*.*'] + schedule: + - cron: '0 4 * * *' + workflow_dispatch: + inputs: + force: + description: Build a nightly even if main has not changed since the last one + type: boolean + default: false + dry_run: + description: "Dry run: build, package and sign, but publish nothing (the files are the run's artifacts)" + type: boolean + default: false + rehearse_tag: + description: "With dry run: rehearse this release tag (e.g. v0.1.0) instead of a nightly" + type: string + default: '' + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + KEEP_NIGHTLIES: 14 + +jobs: + plan: + name: Plan + # The schedule stays off until nightlies are turned on (docs/releasing.md), so merging this workflow publishes nothing. + if: github.event_name != 'schedule' || vars.NIGHTLIES == 'on' + runs-on: ubuntu-latest + outputs: + channel: ${{ steps.plan.outputs.channel }} + version: ${{ steps.plan.outputs.version }} + tag: ${{ steps.plan.outputs.tag }} + previous: ${{ steps.plan.outputs.previous }} + sha: ${{ steps.plan.outputs.sha }} + skip: ${{ steps.plan.outputs.skip }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check the signing key is set up + env: + SECRET: ${{ secrets.MINISIGN_SECRET_KEY }} + PUBLIC: ${{ vars.MINISIGN_PUBLIC_KEY }} + run: | + if [ -z "$SECRET" ] || [ -z "$PUBLIC" ]; then + echo "::error::Set the repository secret MINISIGN_SECRET_KEY and the variable MINISIGN_PUBLIC_KEY (docs/releasing.md)" + exit 1 + fi + + - name: Version + id: plan + env: + FORCE: ${{ inputs.force }} + DRY_RUN: ${{ inputs.dry_run }} + REHEARSE_TAG: ${{ inputs.rehearse_tag }} + run: bash tools/package/plan.sh + + build: + name: ${{ matrix.name }} + needs: plan + if: needs.plan.outputs.skip != 'true' + runs-on: ${{ matrix.os }} + strategy: + fail-fast: true + matrix: + include: + - name: Windows + os: windows-latest + platform: windows-x86_64 + # An older Ubuntu than CI's: the AppImage runs where its C and C++ runtimes are at least this old. + - name: Linux + os: ubuntu-22.04 + platform: linux-x86_64 + - name: macOS + os: macos-latest + platform: macos-aarch64 + env: + LEMON_CHANNEL: ${{ needs.plan.outputs.channel }} + LEMON_VERSION: ${{ needs.plan.outputs.version }} + LEMON_UPDATE_KEY: ${{ vars.MINISIGN_PUBLIC_KEY }} + PLATFORM: ${{ matrix.platform }} + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.plan.outputs.sha }} + + - name: Install Linux packages + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends libasound2-dev libjack-jackd2-dev libwayland-dev libclang-dev pkg-config + + - uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + key: release + + # First, so that Lemon Studio is built carrying it (crates/os-app/build.rs reads LEMON_UPDATER). + - name: Build the updater + run: cargo build --release --locked -p updater + + - name: Build Lemon Studio + run: | + set -euo pipefail + ext=""; [ "$RUNNER_OS" = Windows ] && ext=.exe + export LEMON_UPDATER="$GITHUB_WORKSPACE/target/release/lemonstudio-updater$ext" + if [ "$RUNNER_OS" = macOS ]; then + cargo run --release --locked -p bundle-app + else + cargo build --release --locked -p os-app --bin lemonstudio + fi + + - name: Build the plugins + run: cargo run --release --locked -p bundle-plugins + + - name: Package + run: | + set -euo pipefail + v=$LEMON_VERSION + # No version in the program's name: an update replaces the file in place, so shortcuts to it never break. + program=LemonStudio; [ "$LEMON_CHANNEL" = nightly ] && program=LemonStudioNightly + mkdir -p dist "plugins/LemonStudio-Plugins-$v/VST3" "plugins/LemonStudio-Plugins-$v/CLAP" + case "$RUNNER_OS" in + Windows) + cp target/release/lemonstudio.exe "dist/$program.exe" + ;; + Linux) + # A fixed version with its checksum: this goes into a release that installed copies trust. + curl -fsSL -o "$RUNNER_TEMP/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage + echo "ed4ce84f0d9caff66f50bcca6ff6f35aae54ce8135408b3fa33abfc3cb384eb0 $RUNNER_TEMP/appimagetool" | sha256sum -c - + chmod +x "$RUNNER_TEMP/appimagetool" + APPIMAGE_EXTRACT_AND_RUN=1 APPIMAGETOOL="$RUNNER_TEMP/appimagetool" \ + bash tools/package/appimage.sh target/release/lemonstudio "$v" "dist/$program.AppImage" "$LEMON_CHANNEL" + ;; + macOS) + app="Lemon Studio"; [ "$LEMON_CHANNEL" = nightly ] && app="Lemon Studio Nightly" + ditto -c -k --keepParent "target/bundled/$app.app" "dist/LemonStudio-$v-$PLATFORM.zip" + ;; + esac + p="plugins/LemonStudio-Plugins-$v" + for b in target/bundled/*.vst3; do cp -R "$b" "$p/VST3/"; done + for b in target/bundled/*.clap; do cp -R "$b" "$p/CLAP/"; done + cp tools/package/plugins-readme.txt "$p/README.txt" + cp LICENSE THIRD-PARTY-NOTICES.txt "$p/" + case "$RUNNER_OS" in + Windows) (cd plugins && 7z a -tzip -r "../dist/LemonStudio-Plugins-$v-$PLATFORM.zip" "LemonStudio-Plugins-$v" >/dev/null) ;; + macOS) ditto -c -k --keepParent "$p" "dist/LemonStudio-Plugins-$v-$PLATFORM.zip" ;; + *) (cd plugins && zip -qry "../dist/LemonStudio-Plugins-$v-$PLATFORM.zip" "LemonStudio-Plugins-$v") ;; + esac + ls -l dist + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.platform }} + path: dist/* + if-no-files-found: error + + publish: + name: Publish + needs: [plan, build] + runs-on: ubuntu-latest + env: + GH_TOKEN: ${{ github.token }} + CHANNEL: ${{ needs.plan.outputs.channel }} + VERSION: ${{ needs.plan.outputs.version }} + TAG: ${{ needs.plan.outputs.tag }} + PREVIOUS: ${{ needs.plan.outputs.previous }} + SHA: ${{ needs.plan.outputs.sha }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.plan.outputs.sha }} + + - uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Release notes + run: | + set -euo pipefail + # The first build on a channel has nothing to compare with: GitHub would list every pull request there has been. + if [ -n "$PREVIOUS" ]; then + gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" -f "tag_name=$TAG" -f "target_commitish=$SHA" \ + -f "previous_tag_name=$PREVIOUS" --jq .body >"$RUNNER_TEMP/changes.md" + else + echo "The first build on this channel." >"$RUNNER_TEMP/changes.md" + fi + case "$CHANNEL:$VERSION" in + nightly:*) intro="A nightly build of main at ${SHA:0:8}: the newest changes, and the least tested. It has its own name and icon, and sits beside Lemon Studio if you have both." ;; + stable:0.*) intro="Lemon Studio $VERSION, an alpha." ;; + *) intro="Lemon Studio $VERSION." ;; + esac + python3 - "$RUNNER_TEMP/changes.md" "$intro" >"$RUNNER_TEMP/notes.md" <<'PY' + import os, sys + changes = open(sys.argv[1], encoding="utf-8").read().strip() + text = open("tools/package/release-notes.md", encoding="utf-8").read() + app = "Lemon Studio Nightly" if os.environ["CHANNEL"] == "nightly" else "Lemon Studio" + program = "LemonStudioNightly" if os.environ["CHANNEL"] == "nightly" else "LemonStudio" + print(text.replace("{INTRO}", sys.argv[2]).replace("{VERSION}", os.environ["VERSION"]).replace("{APP}", app).replace("{PROGRAM}", program).replace("{CHANGES}", changes)) + PY + cp LICENSE THIRD-PARTY-NOTICES.txt dist/ + + - name: Manifest + env: + MINISIGN_SECRET_KEY: ${{ secrets.MINISIGN_SECRET_KEY }} + MINISIGN_PUBLIC_KEY: ${{ vars.MINISIGN_PUBLIC_KEY }} + run: | + set -euo pipefail + python3 tools/package/manifest.py --channel "$CHANNEL" --version "$VERSION" --commit "$SHA" \ + --repo "$GITHUB_REPOSITORY" --tag "$TAG" --notes "$RUNNER_TEMP/changes.md" --dist dist --out "dist/$CHANNEL.json" + sudo apt-get update -qq && sudo apt-get install -y -qq minisign + key="$RUNNER_TEMP/minisign.key" + umask 077 + printf '%s\n' "$MINISIGN_SECRET_KEY" >"$key" + minisign -S -s "$key" -m "dist/$CHANNEL.json" -t "lemonstudio $CHANNEL $VERSION" + rm -f "$key" + # The signature must check out against the key the updater was built with, or no installed copy could update. + public=$(printf '%s\n' "$MINISIGN_PUBLIC_KEY" | grep -v '^untrusted comment' | grep . | tail -n1) + minisign -V -P "$public" -m "dist/$CHANNEL.json" + cat "dist/$CHANNEL.json" + + - name: Keep the dry run's files + if: inputs.dry_run + uses: actions/upload-artifact@v4 + with: + name: dry-run-${{ needs.plan.outputs.version }} + path: | + dist/* + ${{ runner.temp }}/notes.md + + - name: Publish the release + if: ${{ !inputs.dry_run }} + run: | + set -euo pipefail + if [ "$CHANNEL" = stable ]; then + title="Lemon Studio $VERSION" + case "$VERSION" in 0.*) title="$title (alpha)" ;; esac + # Not a GitHub pre-release, alpha or not: the updater finds it as GitHub's "latest" release. + gh release create "$TAG" dist/* --verify-tag --latest --title "$title" --notes-file "$RUNNER_TEMP/notes.md" + else + gh release create "$TAG" dist/* --target "$SHA" --prerelease --latest=false \ + --title "Lemon Studio nightly $VERSION" --notes-file "$RUNNER_TEMP/notes.md" + # The fixed place the updater reads: moved on to this nightly once its downloads are all there. + if ! gh release view nightly >/dev/null 2>&1; then + gh release create nightly --target "$SHA" --prerelease --latest=false --title "Nightly (newest)" \ + --notes "Holds nightly.json, which points Lemon Studio's updater at the newest nightly. The downloads are in the dated nightly-YEAR.MONTH.DAY.N releases." + fi + gh release upload nightly dist/nightly.json dist/nightly.json.minisig --clobber + fi + + - name: Delete old nightlies + if: needs.plan.outputs.channel == 'nightly' && !inputs.dry_run + run: | + set -euo pipefail + gh release list --limit 1000 --json tagName --jq '.[].tagName' \ + | { grep -E '^nightly-[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' || true; } | sort -V | head -n "-$KEEP_NIGHTLIES" \ + | while read -r t; do echo "deleting $t"; gh release delete "$t" --cleanup-tag --yes; done diff --git a/Cargo.lock b/Cargo.lock index afc7a70a8..f40d0c5f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,6 +413,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bindgen" version = "0.70.1" @@ -820,6 +826,15 @@ dependencies = [ "windows-core", ] +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1492,6 +1507,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "getrandom" version = "0.3.4" @@ -1587,6 +1613,22 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "iana-time-zone" version = "0.1.65" @@ -2352,6 +2394,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2953,6 +3001,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e036b411754132fd6b74e94b74e9a13c5cf606fa93d1885c35860d3060820b1" + [[package]] name = "orbclient" version = "0.3.55" @@ -3636,6 +3690,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.28" @@ -3689,6 +3757,53 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "rustls" +version = "0.23.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -3704,6 +3819,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cb23c424d8ee841bb4d813677ca237e2a610067d594fec130f87d6093495ab5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -3729,6 +3853,29 @@ dependencies = [ "tiny-skia", ] +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.2", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.28" @@ -3795,6 +3942,17 @@ dependencies = [ "syn 3.0.6", ] +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" @@ -3969,6 +4127,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "swash" version = "0.2.10" @@ -4476,6 +4640,56 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "updater" +version = "0.0.1" +dependencies = [ + "libc", + "minisign-verify", + "rustls", + "rustls-native-certs", + "semver", + "serde", + "serde_json", + "sha2", + "ureq", + "windows-sys 0.61.2", +] + +[[package]] +name = "ureq" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7be76686bdf7198002062423ca0d8cf9b75a27c8e1de804513b97e97b555d9d" +dependencies = [ + "base64", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622dce33c252115466d83f63e9744a18f12b4406aae51a01c15ad8fcf1185eb3" +dependencies = [ + "base64", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4495,6 +4709,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -5372,6 +5592,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + [[package]] name = "zerotrie" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 7a9f8263f..f1f9f5b96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["crates/os-model", "crates/os-flp", "crates/os-engine", "crates/os-dsp", "tools/flp-dump", "tools/os-render", "tools/os-play", "tools/bundle-app", "crates/os-native", "tools/os-native-probe", "crates/os-theme", "crates/os-ui", "crates/os-app", "plugins/lemon-sampler", "tools/bundle-plugins", "crates/os-clap", "crates/os-hostlink", "crates/os-plugkit", "plugins/lemon-parametric-eq-2", "crates/os-host", "crates/os-vst3", "crates/os-vst2", "plugins/lemon-3x-osc", "plugins/lemon-limiter", "plugins/lemon-reverb", "plugins/lemon-delay", "plugins/lemon-small-fx", "plugins/lemon-soft-clipper", "plugins/lemon-fast-distortion", "plugins/lemon-balance", "plugins/lemon-stereo-enhancer", "plugins/lemon-transient-processor", "plugins/lemon-chorus", "plugins/lemon-stereo-shaper", "plugins/lemon-filter", "plugins/citrus", "plugins/smith", "plugins/tesla", "plugins/pulp-filter", "plugins/press", "crates/os-paths", "tools/watermark", "crates/os-smf", "tools/notices", "plugins/lemon-flanger", "plugins/lemon-phaser", "plugins/lemon-peak-controller", "plugins/lemon-formula-controller", "plugins/lemon-slicer", "plugins/lemon-vocoder", "plugins/lemon-drum-sampler", "plugins/lemon-drop"] +members = ["crates/os-model", "crates/os-flp", "crates/os-engine", "crates/os-dsp", "tools/flp-dump", "tools/os-render", "tools/os-play", "tools/bundle-app", "crates/os-native", "tools/os-native-probe", "crates/os-theme", "crates/os-ui", "crates/os-app", "plugins/lemon-sampler", "tools/bundle-plugins", "crates/os-clap", "crates/os-hostlink", "crates/os-plugkit", "plugins/lemon-parametric-eq-2", "crates/os-host", "crates/os-vst3", "crates/os-vst2", "plugins/lemon-3x-osc", "plugins/lemon-limiter", "plugins/lemon-reverb", "plugins/lemon-delay", "plugins/lemon-small-fx", "plugins/lemon-soft-clipper", "plugins/lemon-fast-distortion", "plugins/lemon-balance", "plugins/lemon-stereo-enhancer", "plugins/lemon-transient-processor", "plugins/lemon-chorus", "plugins/lemon-stereo-shaper", "plugins/lemon-filter", "plugins/citrus", "plugins/smith", "plugins/tesla", "plugins/pulp-filter", "plugins/press", "crates/os-paths", "tools/watermark", "crates/os-smf", "tools/notices", "tools/updater", "plugins/lemon-flanger", "plugins/lemon-phaser", "plugins/lemon-peak-controller", "plugins/lemon-formula-controller", "plugins/lemon-slicer", "plugins/lemon-vocoder", "plugins/lemon-drum-sampler", "plugins/lemon-drop"] [workspace.package] version = "0.0.1" diff --git a/README.md b/README.md index ad2f9dca9..145271864 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,7 @@ young, so read the warning below before trying it on real work. > - **Greyed-out controls are not implemented yet.** Many menus and buttons are already laid out but do > nothing so far; they are drawn dim and say "not available yet". Other features of a full DAW have no control > at all yet, so a menu with nothing greyed in it is not necessarily complete. -> - **No prebuilt binaries** will be published until Lemon Studio reaches a usable state. Until then it -> can only be built from source. +> - **Builds are not signed.** Your system may stop them the first time; the release notes say how to open them. > - **Projects saved by FL Studio 10 or later open** (25 and 26 included). Projects older than 24.2 are > converted to the current layout as they open and are saved in it, so the version that wrote them may not > read the saved copy. @@ -120,7 +119,14 @@ Per-area details are in [docs/status.md](docs/status.md). The full, item-by-item ## Getting started -No prebuilt binaries are provided at this stage: Lemon Studio is built from source. Each platform below goes from a fresh +**Builds** for Windows, Linux (AppImage) and macOS (Apple silicon) are on the +[releases page](https://github.com/ItsAlphaNeon/Lemon-Studio/releases), with Lemon Studio's plugins for other DAWs as a +separate zip: **alphas**, the tagged `0.x` releases, and **nightlies**, the newest changes on main, built every night. +They are not signed: the release notes say how to open them on each system (on macOS, past Gatekeeper). Once installed, +a build checks for a newer one of its own kind when it starts and installs it when you agree (Settings ▸ General ▸ +Updates). How releases are made: [docs/releasing.md](docs/releasing.md). + +Or build Lemon Studio from source, which never updates itself. Each platform below goes from a fresh machine to Lemon Studio installed with its icon, and Lemon Studio's own plugins installed for any DAW. All three need **Rust**, stable toolchain 1.89 or newer, installed through [rustup](https://rustup.rs/), and a clone of this repository: @@ -358,6 +364,7 @@ cargo run -p os-app --release -- --control /tmp/lemon.sock path/to/project.flp | `os-play` | Play a project through the default audio output | | `bundle-plugins` | Build Lemon Studio's own plugins as VST3 and CLAP bundles, and optionally install them (`cargo run -p os-app --release` runs it with `--install`) | | `bundle-app` | Build Lemon Studio as a macOS app, and optionally install it | +| `updater` | `lemonstudio-updater`, which release builds carry to update themselves (built by the release workflow only; [docs/releasing.md](docs/releasing.md)) | ## Architecture diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 77abbc3e1..4f35cddc1 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -917,6 +917,7 @@ listed too. autocfg 1.5.1 (Apache-2.0 OR MIT) autotools 0.2.7 (MIT) backtrace 0.3.76 (MIT OR Apache-2.0) + base64 0.23.1 (MIT OR Apache-2.0) bindgen 0.70.1 (BSD-3-Clause) bindgen 0.72.1 (BSD-3-Clause) bitflags 1.3.2 (MIT OR Apache-2.0) @@ -954,6 +955,7 @@ listed too. coremidi 0.9.2 (MIT) coremidi-sys 3.2.1 (MIT) cpal 0.18.2 (Apache-2.0) + cpufeatures 0.2.17 (MIT OR Apache-2.0) crc 3.4.0 (MIT OR Apache-2.0) crc-catalog 2.5.0 (MIT OR Apache-2.0) crc32fast 1.5.2 (MIT OR Apache-2.0) @@ -1024,6 +1026,7 @@ listed too. futures-util 0.3.34 (MIT OR Apache-2.0) generic-array 0.14.7 (MIT) gethostname 1.1.0 (Apache-2.0) + getrandom 0.2.17 (MIT OR Apache-2.0) getrandom 0.3.4 (MIT OR Apache-2.0) getrandom 0.4.3 (MIT OR Apache-2.0) gimli 0.32.3 (MIT OR Apache-2.0) @@ -1035,6 +1038,8 @@ listed too. hermit-abi 0.1.19 (MIT OR Apache-2.0) hermit-abi 0.5.3 (MIT OR Apache-2.0) hex 0.4.3 (MIT OR Apache-2.0) + http 1.5.0 (MIT OR Apache-2.0) + httparse 1.10.1 (MIT OR Apache-2.0) iana-time-zone 0.1.65 (MIT OR Apache-2.0) iana-time-zone-haiku 0.1.2 (MIT OR Apache-2.0) icu_collections 2.3.0 (Unicode-3.0) @@ -1083,6 +1088,7 @@ listed too. midi-consts 0.1.0 (MIT OR Apache-2.0) midir 0.11.1 (MIT) minimal-lexical 0.2.1 (MIT OR Apache-2.0) + minisign-verify 0.2.5 (MIT) miniz_oxide 0.8.9 (MIT OR Zlib OR Apache-2.0) miniz_oxide 0.9.1 (MIT OR Zlib OR Apache-2.0) mio 1.2.3 (MIT) @@ -1123,6 +1129,7 @@ listed too. object 0.37.3 (Apache-2.0 OR MIT) ogg_next_sys 0.1.5 (BSD-3-Clause) once_cell 1.21.4 (MIT OR Apache-2.0) + openssl-probe 0.2.2 (MIT OR Apache-2.0) orbclient 0.3.55 (MIT) ordered-stream 0.2.0 (MIT OR Apache-2.0) owned_ttf_parser 0.25.1 (Apache-2.0) @@ -1166,17 +1173,25 @@ listed too. regex-lite 0.1.9 (MIT OR Apache-2.0) regex-syntax 0.8.11 (MIT OR Apache-2.0) rfd 0.15.4 (MIT) + ring 0.17.14 (Apache-2.0 AND ISC) rustc-demangle 0.1.28 (MIT OR Apache-2.0) rustc-hash 1.1.0 (Apache-2.0 OR MIT) rustc-hash 2.1.3 (Apache-2.0 OR MIT) rustc_version 0.4.1 (MIT OR Apache-2.0) rustix 0.38.44 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) rustix 1.1.5 (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) + rustls 0.23.46 (Apache-2.0 OR ISC OR MIT) + rustls-native-certs 0.8.5 (Apache-2.0 OR ISC OR MIT) + rustls-pki-types 1.15.1 (MIT OR Apache-2.0) + rustls-webpki 0.103.16 (ISC) rustversion 1.0.23 (MIT OR Apache-2.0) same-file 1.0.6 (Unlicense OR MIT) + schannel 0.1.30 (MIT) scoped-tls 1.0.1 (MIT OR Apache-2.0) scopeguard 1.2.0 (MIT OR Apache-2.0) sctk-adwaita 0.10.1 (MIT) + security-framework 3.7.0 (MIT OR Apache-2.0) + security-framework-sys 2.17.0 (MIT OR Apache-2.0) semver 1.0.28 (MIT OR Apache-2.0) seq-macro 0.3.6 (MIT OR Apache-2.0) serde 1.0.229 (MIT OR Apache-2.0) @@ -1184,6 +1199,7 @@ listed too. serde_derive 1.0.229 (MIT OR Apache-2.0) serde_json 1.0.151 (MIT OR Apache-2.0) serde_repr 0.1.21 (MIT OR Apache-2.0) + sha2 0.10.9 (MIT OR Apache-2.0) shlex 1.3.0 (MIT OR Apache-2.0) shlex 2.0.1 (MIT OR Apache-2.0) signal-hook-registry 1.4.8 (MIT OR Apache-2.0) @@ -1199,6 +1215,7 @@ listed too. softbuffer 0.4.8 (MIT OR Apache-2.0) stable_deref_trait 1.2.1 (MIT OR Apache-2.0) strict-num 0.1.1 (MIT) + subtle 2.6.1 (BSD-3-Clause) swash 0.2.10 (Apache-2.0 OR MIT) symphonia 0.6.1 (MPL-2.0) symphonia-bundle-flac 0.6.1 (MPL-2.0) @@ -1245,8 +1262,12 @@ listed too. uds_windows 1.2.1 (MIT) unicode-ident 1.0.26 ((MIT OR Apache-2.0) AND Unicode-3.0) unicode-segmentation 1.13.3 (MIT OR Apache-2.0) + untrusted 0.9.0 (ISC) + ureq 3.5.1 (MIT OR Apache-2.0) + ureq-proto 0.7.1 (MIT OR Apache-2.0) url 2.5.8 (MIT OR Apache-2.0) urlencoding 2.1.3 (MIT) + utf8-zero 0.8.1 (MIT OR Apache-2.0) utf8_iter 1.0.4 (Apache-2.0 OR MIT) uuid 1.27.0 (Apache-2.0 OR MIT) version_check 0.9.5 (MIT OR Apache-2.0) @@ -1333,6 +1354,7 @@ listed too. zerocopy 0.8.62 (BSD-2-Clause OR Apache-2.0 OR MIT) zerofrom 0.1.8 (Unicode-3.0) zerofrom-derive 0.1.8 (Unicode-3.0) + zeroize 1.9.1 (Apache-2.0 OR MIT) zerotrie 0.2.5 (Unicode-3.0) zerovec 0.11.8 (Unicode-3.0) zerovec-derive 0.11.6 (Unicode-3.0) @@ -1552,6 +1574,282 @@ The licence texts follow, each after the crates it covers. limitations under the License. +--- Apache License 2.0: ring 0.17.14 + + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +Licenses for support code +------------------------- + +Parts of the TLS test suite are under the Go license. This code is not included +in BoringSSL (i.e. libcrypto and libssl) when compiled, however, so +distributing code linked against BoringSSL does not trigger this license: + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +BoringSSL uses the Chromium test infrastructure to run a continuous build, +trybots etc. The scripts which manage this, and the script for generating build +metadata, are under the Chromium license. Distributing code linked against +BoringSSL does not trigger this license. + +Copyright 2015 The Chromium Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + --- Apache License 2.0: zopfli 0.8.4 Apache License @@ -1962,7 +2260,7 @@ The licence texts follow, each after the crates it covers. limitations under the License. ---- Apache License 2.0: gethostname 1.1.0 +--- Apache License 2.0: gethostname 1.1.0, ring 0.17.14 Apache License Version 2.0, January 2004 @@ -2512,39 +2810,6 @@ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- BSD 3-Clause "New" or "Revised" License: aotuv_lancer_vorbis_sys 0.1.6, - ogg_next_sys 0.1.5 - -Copyright (c) 2002, Xiph.org Foundation - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -- Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -- Neither the name of the Xiph.org Foundation nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION -OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - --- BSD 3-Clause "New" or "Revised" License: tiny-skia-path 0.11.4, tiny-skia 0.11.4 @@ -2580,7 +2845,41 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- BSD 3-Clause "New" or "Revised" License: vorbis_rs 0.5.6 +--- BSD 3-Clause "New" or "Revised" License: subtle 2.6.1 + +Copyright (c) 2016-2017 Isis Agora Lovecruft, Henry de Valence. All rights reserved. +Copyright (c) 2016-2024 Isis Agora Lovecruft. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +--- BSD 3-Clause "New" or "Revised" License: aotuv_lancer_vorbis_sys 0.1.6, + ogg_next_sys 0.1.5, vorbis_rs 0.5.6 Copyright (c) . @@ -2595,39 +2894,6 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ---- BSD 3-Clause "New" or "Revised" License: aotuv_lancer_vorbis_sys 0.1.6 - -aoTuV - Copyright (c) 2003-2021 Aoyumi -Copyright (c) 2002-2020 Xiph.org Foundation - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions -are met: - -- Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - -- Redistributions in binary form must reproduce the above copyright -notice, this list of conditions and the following disclaimer in the -documentation and/or other materials provided with the distribution. - -- Neither the name of the Xiph.org Foundation nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION -OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - --- GNU General Public License v3.0 or later: symphonia-codec-wavpack 0.1.1 GNU GENERAL PUBLIC LICENSE @@ -2841,6 +3107,640 @@ This program is distributed in the hope that it will be useful,but WITHOUT ANY W You should have received a copy of the GNU General Public License along with this program. If not, see . +--- ISC License: ring 0.17.14 + +/* Copyright (c) 2014, Intel Corporation. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ + +#ifndef OPENSSL_HEADER_EC_ECP_NISTZ384_H +#define OPENSSL_HEADER_EC_ECP_NISTZ384_H + +#include "../../limbs/limbs.h" + +#define P384_LIMBS (384u / LIMB_BITS) + +typedef struct { + Limb X[P384_LIMBS]; + Limb Y[P384_LIMBS]; + Limb Z[P384_LIMBS]; +} P384_POINT; + +typedef struct { + Limb X[P384_LIMBS]; + Limb Y[P384_LIMBS]; +} P384_POINT_AFFINE; + + +#endif // OPENSSL_HEADER_EC_ECP_NISTZ384_H + + +--- ISC License: ring 0.17.14 + +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//! EdDSA Signatures. + +use super::ops::ELEM_LEN; +use crate::digest; + +pub mod signing; +pub mod verification; + +/// The length of an Ed25519 public key. +pub const ED25519_PUBLIC_KEY_LEN: usize = ELEM_LEN; + +pub fn eddsa_digest(signature_r: &[u8], public_key: &[u8], msg: &[u8]) -> digest::Digest { + let mut ctx = digest::Context::new(&digest::SHA512); + ctx.update(signature_r); + ctx.update(public_key); + ctx.update(msg); + ctx.finish() +} + + +--- ISC License: untrusted 0.9.0 + +// Copyright 2015-2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + +--- ISC License: ring 0.17.14 + +// Copyright 2015-2022 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::limb::Limb; + +#[derive(Clone, Copy)] +#[repr(transparent)] +pub struct N0([Limb; 2]); + +impl N0 { + #[cfg(feature = "alloc")] + pub(super) const LIMBS_USED: usize = 64 / crate::limb::LIMB_BITS; + + #[inline] + pub const fn precalculated(n0: u64) -> Self { + #[cfg(target_pointer_width = "64")] + { + Self([n0, 0]) + } + + #[cfg(target_pointer_width = "32")] + { + Self([n0 as Limb, (n0 >> crate::limb::LIMB_BITS) as Limb]) + } + } +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2015-2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#[cfg(target_pointer_width = "64")] +type CompilerWord = u64; + +#[cfg(target_pointer_width = "32")] +type CompilerWord = u32; + +/// A native word that isn't secret. +/// +/// `LeakyWord` supports `as` conversions to/from native types. +/// +/// XXX: This isn't the native word size on targets where a pointer isn't the +/// same size as a native word. TODO: Fix this. +pub(crate) type LeakyWord = CompilerWord; + + +--- ISC License: ring 0.17.14 + +// Copyright 2015-2025 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::{bb, error}; + +#[deprecated( + note = "To be removed. Internal function not intended for external use with no promises regarding side channels." +)] +pub fn verify_slices_are_equal(a: &[u8], b: &[u8]) -> Result<(), error::Unspecified> { + bb::verify_slices_are_equal(a, b) +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2016 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//! Elliptic curve operations and schemes using Curve25519. + +pub mod ed25519; + +pub mod x25519; + +mod ops; +mod scalar; + + +--- ISC License: ring 0.17.14 + +// Copyright 2016-2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::error::{KeyRejected, Unspecified}; + +impl From for Unspecified { + fn from(source: untrusted::EndOfInput) -> Self { + super::erase(source) + } +} + +impl From for Unspecified { + fn from(source: core::array::TryFromSliceError) -> Self { + super::erase(source) + } +} + +impl From for Unspecified { + fn from(source: KeyRejected) -> Self { + super::erase(source) + } +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2018 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#[cfg(target_arch = "x86")] +pub fn shift_full_blocks( + in_out: super::overlapping::Overlapping<'_, u8>, + mut transform: impl FnMut(&[u8; BLOCK_LEN]) -> [u8; BLOCK_LEN], +) { + let (in_out, src) = in_out.into_slice_src_mut(); + let in_out_len = in_out[src.clone()].len(); + + for i in (0..in_out_len).step_by(BLOCK_LEN) { + let block = { + let input = + <&[u8; BLOCK_LEN]>::try_from(&in_out[(src.start + i)..][..BLOCK_LEN]).unwrap(); + transform(input) + }; + let output = <&mut [u8; BLOCK_LEN]>::try_from(&mut in_out[i..][..BLOCK_LEN]).unwrap(); + *output = block; + } +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2018 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//! Serialization and deserialization. + +#[doc(hidden)] +pub mod der; + +#[cfg(feature = "alloc")] +mod writer; + +#[cfg(feature = "alloc")] +pub(crate) mod der_writer; + +pub(crate) mod positive; + +pub use self::positive::Positive; + +#[cfg(feature = "alloc")] +pub(crate) use self::writer::TooLongError; + + +--- ISC License: ring 0.17.14 + +// Copyright 2019-2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use super::BlockLen; + +pub(super) use self::{ + sha2_32::{block_data_order_32, State32, SHA256_BLOCK_LEN}, + sha2_64::{block_data_order_64, State64, SHA512_BLOCK_LEN}, +}; + +pub(super) const CHAINING_WORDS: usize = 8; + +#[cfg(any( + all(target_arch = "aarch64", target_endian = "little"), + all(target_arch = "arm", target_endian = "little"), + target_arch = "x86_64" +))] +#[macro_use] +mod ffi; + +pub(super) mod fallback; +mod sha2_32; +mod sha2_64; + + +--- ISC License: ring 0.17.14 + +// Copyright 2022 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +/// Polyfill for `Option::unwrap()` as a const fn; feature `const_option`. +/// https://github.com/rust-lang/rust/issues/67441. +/// TODO(MSRV): Replace this with `x.unwrap()`. +/// +/// `T: Copy` avoids "constant functions cannot evaluate destructors." +pub const fn unwrap_const(x: Option) -> T +where + T: Copy, +{ + if let Some(x) = x { + x + } else { + panic!("unwrap_const on `None`"); + } +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +// TODO(MSRV 1.76): Replace with `core::ptr::from_mut`. +#[allow(dead_code)] +#[inline(always)] +pub fn from_mut(r: &mut T) -> *mut T { + r +} + +// TODO(MSRV 1.76): Replace with `core::ptr::from_ref`. +#[allow(dead_code)] +#[inline(always)] +pub const fn from_ref(r: &T) -> *const T { + r +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//! Integration tests for non-public APIs. + +mod bits_tests; + + +--- ISC License: ring 0.17.14 + +// Copyright 2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +//! Utilities to make dealing with slices less tediuous. + +/// Replaces the first N elements of `a` with the first N elements of `b`, where +/// N is `core::cmp::min(a.len(), b.len())`, leaving the rest unchanged. +pub fn overwrite_at_start(a: &mut [T], b: &[T]) { + a.iter_mut().zip(b).for_each(|(a, b)| { + *a = *b; + }); +} + + +--- ISC License: ring 0.17.14 + +// Copyright 2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +pub use self::{ + array::Array, + base::{IndexError, Overlapping}, + partial_block::PartialBlock, +}; + +mod array; +mod base; +mod partial_block; + + +--- ISC License: ring 0.17.14 + +// Copyright 2024 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use crate::testutil; +use core::{marker::PhantomData, mem::size_of}; + +/// A ZST that can be added to any type to make the type `!Send`. +#[derive(Clone, Copy)] +pub struct NotSend(PhantomData<*mut ()>); + +impl NotSend { + pub const VALUE: Self = Self(PhantomData); +} + +#[allow(deprecated)] +const _: () = testutil::compile_time_assert_clone::(); +#[allow(deprecated)] +const _: () = testutil::compile_time_assert_copy::(); +const _: () = assert!(size_of::() == 0); + + +--- ISC License: ring 0.17.14 + +// Copyright 2025 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#![cfg(all(target_arch = "aarch64", target_endian = "little"))] + +pub(in super::super) mod mont; + + +--- ISC License: ring 0.17.14 + +// Copyright 2025 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +#![cfg(target_arch = "x86_64")] + +pub(in super::super::super) mod mont; + + +--- ISC License: ring 0.17.14 + +// Copyright 2025 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +mod storage; + +pub(super) use self::storage::{AlignedStorage, LIMBS_PER_CHUNK}; + + +--- ISC License: ring 0.17.14 + +// Copyright 2025 Brian Smith. +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +pub(super) mod aarch64; +pub(super) mod x86_64; + + +--- ISC License: ring 0.17.14 + +Copyright 2015-2025 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + --- ISC License: libloading 0.7.4, libloading 0.8.9 Copyright © 2015, Simonas Kazlauskas @@ -2857,6 +3757,29 @@ NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE US THIS SOFTWARE. +--- ISC License: rustls-webpki 0.103.16 + +Except as otherwise noted, this project is licensed under the following +(ISC-style) terms: + +Copyright 2015 Brian Smith. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHORS DISCLAIM ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +The files under third-party/chromium are licensed as described in +third-party/chromium/LICENSE. + + --- ISC License: nih_plug 0.0.0 ISC License @@ -2897,14 +3820,13 @@ PERFORMANCE OF THIS SOFTWARE. --- ISC License: nih_plug_derive 0.1.0 -ISC License: +ISC License -Copyright (c) 2004-2010 by Internet Systems Consortium, Inc. ("ISC") -Copyright (c) 1995-2003 by Internet Software Consortium +Copyright Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. -THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. --- GNU Lesser General Public License v3.0 only: mp3lame-encoder 0.2.5, @@ -3374,7 +4296,7 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: md-5 0.10.6 +--- MIT License: md-5 0.10.6, sha2 0.10.9 Copyright (c) 2006-2009 Graydon Hoare Copyright (c) 2009-2013 Mozilla Foundation @@ -3519,10 +4441,10 @@ DEALINGS IN THE SOFTWARE. --- MIT License: backtrace 0.3.76, cc 1.6.0, cfg-if 1.0.5, find-msvc-tools 0.1.14, jobserver 0.1.35, js-sys 0.3.106, - pkg-config 0.3.34, rustc-demangle 0.1.28, scoped-tls 1.0.1, - wasm-bindgen-futures 0.4.79, wasm-bindgen-macro-support 0.2.129, - wasm-bindgen-macro 0.2.129, wasm-bindgen-shared 0.2.129, - wasm-bindgen 0.2.129, web-sys 0.3.106 + openssl-probe 0.2.2, pkg-config 0.3.34, rustc-demangle 0.1.28, + scoped-tls 1.0.1, wasm-bindgen-futures 0.4.79, + wasm-bindgen-macro-support 0.2.129, wasm-bindgen-macro 0.2.129, + wasm-bindgen-shared 0.2.129, wasm-bindgen 0.2.129, web-sys 0.3.106 Copyright (c) 2014 Alex Crichton @@ -3933,6 +4855,40 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: schannel 0.1.30 + +Copyright (c) 2015 steffengy + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--- MIT License: jack-sys 0.5.1 + +Copyright (c) 2015-2016 Patrick Reisert + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --- MIT License: winapi 0.3.9 Copyright (c) 2015-2018 The winapi-rs Developers @@ -3980,6 +4936,29 @@ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: httparse 1.10.1 + +Copyright (c) 2015-2025 Sean McArthur + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + --- MIT License: futures-channel 0.3.34, futures-core 0.3.34, futures-executor 0.3.34, futures-io 0.3.34, futures-macro 0.3.34, futures-sink 0.3.34, futures-task 0.3.34, futures-util 0.3.34, @@ -4042,6 +5021,35 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: rustls-native-certs 0.8.5, rustls 0.23.46 + +Copyright (c) 2016 Joseph Birr-Pixton + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + --- MIT License: lock_api 0.4.14, parking_lot 0.12.5, parking_lot_core 0.9.12, rustc_version 0.4.1 @@ -4457,6 +5465,35 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: http 1.5.0 + +Copyright (c) 2017 http-rs authors + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + --- MIT License: signal-hook-registry 1.4.8 Copyright (c) 2017 tokio-jsonrpc developers @@ -4786,6 +5823,36 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: getrandom 0.2.17 + +Copyright (c) 2018-2024 The rust-random Project Developers +Copyright (c) 2014 The Rust Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + --- MIT License: getrandom 0.3.4 Copyright (c) 2018-2025 The rust-random Project Developers @@ -4816,6 +5883,35 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: zeroize 1.9.1 + +Copyright (c) 2018-2026 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + --- MIT License: getrandom 0.4.3 Copyright (c) 2018-2026 The rust-random Project Developers @@ -5244,6 +6340,35 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: cpufeatures 0.2.17 + +Copyright (c) 2020-2025 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + --- MIT License: num_threads 0.1.7 Copyright (c) 2021 Jacob Pratt @@ -5330,16 +6455,45 @@ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +--- MIT License: rustls-pki-types 1.15.1 + +Copyright (c) 2023 Dirkjan Ochtman + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. --- MIT License: powerfmt 0.2.1 @@ -5794,6 +6948,17 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: ureq-proto 0.7.1 + +Copyright 2022 Martin Algesten + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + --- MIT License: pulseaudio 0.3.2 Copyright 2023 Colin Marc @@ -5874,43 +7039,6 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: wayland-protocols 0.32.13 - -Copyright © 2008-2013 Kristian Høgsberg -Copyright © 2010-2013 Intel Corporation -Copyright © 2013 Rafael Antognolli -Copyright © 2013 Jasper St. Pierre -Copyright © 2014 Jonas Ådahl -Copyright © 2014 Jason Ekstrand -Copyright © 2014-2015 Collabora, Ltd. -Copyright © 2015 Red Hat Inc. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the "Software"), -to deal in the Software without restriction, including without limitation -the rights to use, copy, modify, merge, publish, distribute, sublicense, -and/or sell copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next -paragraph) shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. - ---- - -The above is the version of the MIT "Expat" License used by X.org: - - http://cgit.freedesktop.org/xorg/xserver/tree/COPYING - - --- MIT License: alsa 0.11.0 MIT License @@ -6061,11 +7189,24 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: crc-catalog 2.5.0 +--- MIT License: bytemuck 1.25.2, bytemuck_derive 1.12.1 + +MIT License + +Copyright (c) 2019 Daniel "Lokathor" Gee. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +--- MIT License: fontdue 0.9.4 MIT License -Copyright (c) 2019 Akhil Velagapudi +Copyright (c) 2019 Joe C (mooman219) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -6086,24 +7227,11 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: bytemuck 1.25.2, bytemuck_derive 1.12.1 - -MIT License - -Copyright (c) 2019 Daniel "Lokathor" Gee. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - ---- MIT License: fontdue 0.9.4 +--- MIT License: ureq 3.5.1 MIT License -Copyright (c) 2019 Joe C (mooman219) +Copyright (c) 2019 Martin Algesten Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -6287,30 +7415,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: signalsmith-stretch 0.1.3 - -MIT License - -Copyright (c) 2022 Geraint Luff / Signalsmith Audio Ltd. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - --- MIT License: libredox 0.1.25 MIT License @@ -6410,42 +7514,17 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: signalsmith-stretch 0.1.3 - -MIT License - -Copyright (c) 2025 Signalsmith Audio - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --- MIT License: vst3-com-macros-support 0.2.0, vst3-com-macros 0.2.0, anymap3 1.1.0, block2 0.5.1, block2 0.6.2, cesu8 1.1.0, chrono 0.4.45, - dasp 0.11.0, dasp_envelope 0.11.0, dasp_frame 0.11.0, + crc-catalog 2.5.0, dasp 0.11.0, dasp_envelope 0.11.0, dasp_frame 0.11.0, dasp_interpolate 0.11.0, dasp_peak 0.11.0, dasp_ring_buffer 0.11.0, dasp_rms 0.11.0, dasp_sample 0.11.0, dasp_signal 0.11.0, dasp_slice 0.11.0, dasp_window 0.11.1, dispatch2 0.3.1, dispatch 0.2.0, dlopen2 0.9.0, dpi 0.1.2, drm-fourcc 2.2.0, java-locator 0.1.9, jni-macros 0.22.4, jni-min-helper 0.3.4, jni-sys-macros 0.4.1, jni 0.22.4, libm 0.2.16, - malloc_buf 0.0.6, ndk-context 0.1.1, ndk-sys 0.6.0+11769913, ndk 0.9.0, - objc-sys 0.3.5, objc2-app-kit 0.2.2, objc2-app-kit 0.3.2, - objc2-audio-toolbox 0.3.2, objc2-avf-audio 0.3.2, + malloc_buf 0.0.6, minisign-verify 0.2.5, ndk-context 0.1.1, + ndk-sys 0.6.0+11769913, ndk 0.9.0, objc-sys 0.3.5, objc2-app-kit 0.2.2, + objc2-app-kit 0.3.2, objc2-audio-toolbox 0.3.2, objc2-avf-audio 0.3.2, objc2-core-audio-types 0.3.2, objc2-core-audio 0.3.2, objc2-core-foundation 0.3.2, objc2-core-graphics 0.3.2, objc2-encode 4.1.0, objc2-foundation 0.2.2, objc2-foundation 0.3.2, objc2-quartz-core 0.3.2, @@ -6692,29 +7771,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: wayland-protocols-plasma 0.3.12 - -MIT License Copyright (c) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice (including the next -paragraph) shall be included in all copies or substantial portions of the -Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS -OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF -OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - --- MIT License: rustc-hash 2.1.3 Permission is hereby granted, free of charge, to any @@ -6761,7 +7817,7 @@ DEALINGS IN THE SOFTWARE. serde_repr 0.1.21, simd_cesu8 1.2.0, smol_str 0.2.2, syn 1.0.109, syn 2.0.119, syn 3.0.6, thiserror-impl 1.0.69, thiserror-impl 2.0.21, thiserror 1.0.69, thiserror 2.0.21, typed-path 0.13.0, - unicode-ident 1.0.26, wasi 0.11.1+wasi-snapshot-preview1, + unicode-ident 1.0.26, utf8-zero 0.8.1, wasi 0.11.1+wasi-snapshot-preview1, wasip2 1.0.4+wasi-0.2.12, wit-bindgen 0.57.1, x11-dl 2.21.0, zmij 1.0.23, zvariant_utils 4.2.0 @@ -6933,7 +7989,7 @@ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: tracing-core 0.1.36, zip 9.1.2 +--- MIT License: zip 9.1.2 The MIT License (MIT) @@ -7108,6 +8164,30 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: security-framework-sys 2.17.0, security-framework 3.7.0 + +The MIT License (MIT) + +Copyright (c) 2015 Steven Fackler + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + --- MIT License: orbclient 0.3.55 The MIT License (MIT) @@ -7183,6 +8263,31 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: jack 0.13.5 + +The MIT License (MIT) + +Copyright (c) 2016 Will S Medrano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + --- MIT License: same-file 1.0.6, winapi-util 0.1.11 The MIT License (MIT) @@ -7304,6 +8409,31 @@ The above copyright notice and this permission notice shall be included in all c THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +--- MIT License: base64 0.23.1 + +The MIT License (MIT) + +Copyright (c) 2025 Alice Maz, Marshall Pierce + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + --- MIT License: version_check 0.9.5 The MIT License (MIT) @@ -7427,54 +8557,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ---- MIT License: jack 0.13.5 - -The MIT License (MIT) - -Copyright (c) 2016 Will S Medrano - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - ---- MIT License: jack-sys 0.5.1 - -Copyright (c) 2015-2016 Patrick Reisert - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - --- Mozilla Public License 2.0: symphonia-bundle-flac 0.6.1, symphonia-bundle-mp3 0.6.1, symphonia-codec-aac 0.6.1, symphonia-codec-alac 0.6.1, symphonia-codec-pcm 0.6.1, diff --git a/crates/os-app/assets/lemonstudio-nightly.car b/crates/os-app/assets/lemonstudio-nightly.car new file mode 100644 index 000000000..081e3eddf Binary files /dev/null and b/crates/os-app/assets/lemonstudio-nightly.car differ diff --git a/crates/os-app/assets/lemonstudio-nightly.icns b/crates/os-app/assets/lemonstudio-nightly.icns new file mode 100644 index 000000000..92cbe5176 Binary files /dev/null and b/crates/os-app/assets/lemonstudio-nightly.icns differ diff --git a/crates/os-app/assets/lemonstudio-nightly.ico b/crates/os-app/assets/lemonstudio-nightly.ico new file mode 100644 index 000000000..e22277d45 Binary files /dev/null and b/crates/os-app/assets/lemonstudio-nightly.ico differ diff --git a/crates/os-app/assets/lemonstudio-nightly.png b/crates/os-app/assets/lemonstudio-nightly.png new file mode 100644 index 000000000..dce17dd31 Binary files /dev/null and b/crates/os-app/assets/lemonstudio-nightly.png differ diff --git a/crates/os-app/assets/logo/icon-lemon-nightly.pdn b/crates/os-app/assets/logo/icon-lemon-nightly.pdn new file mode 100644 index 000000000..16ee2c3fb Binary files /dev/null and b/crates/os-app/assets/logo/icon-lemon-nightly.pdn differ diff --git a/crates/os-app/assets/logo/icon-lemon-nightly.png b/crates/os-app/assets/logo/icon-lemon-nightly.png new file mode 100644 index 000000000..09ed609e2 Binary files /dev/null and b/crates/os-app/assets/logo/icon-lemon-nightly.png differ diff --git a/crates/os-app/assets/logo/logo-colored-lemon-nightly.svg b/crates/os-app/assets/logo/logo-colored-lemon-nightly.svg new file mode 100644 index 000000000..83e3b3b9d --- /dev/null +++ b/crates/os-app/assets/logo/logo-colored-lemon-nightly.svg @@ -0,0 +1,25 @@ + + + + + + + + + + logo-colored-lemon-nightly.svg + + + + + + + + + + + + + + + diff --git a/crates/os-app/assets/logo/macOS-nightly.icon/Assets/logo-colored-lemon-nightly.svg b/crates/os-app/assets/logo/macOS-nightly.icon/Assets/logo-colored-lemon-nightly.svg new file mode 100644 index 000000000..83e3b3b9d --- /dev/null +++ b/crates/os-app/assets/logo/macOS-nightly.icon/Assets/logo-colored-lemon-nightly.svg @@ -0,0 +1,25 @@ + + + + + + + + + + logo-colored-lemon-nightly.svg + + + + + + + + + + + + + + + diff --git a/crates/os-app/assets/logo/macOS-nightly.icon/icon.json b/crates/os-app/assets/logo/macOS-nightly.icon/icon.json new file mode 100644 index 000000000..a31996c3d --- /dev/null +++ b/crates/os-app/assets/logo/macOS-nightly.icon/icon.json @@ -0,0 +1,32 @@ +{ + "fill" : { + "automatic-gradient" : "display-p3:0.32178,0.33228,0.36572,1.00000" + }, + "groups" : [ + { + "layers" : [ + { + "blend-mode" : "normal", + "glass" : true, + "image-name" : "logo-colored-lemon-nightly.svg", + "name" : "logo-colored-lemon-nightly", + "opacity" : 1 + } + ], + "shadow" : { + "kind" : "neutral", + "opacity" : 0.5 + }, + "translucency" : { + "enabled" : true, + "value" : 0.5 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/crates/os-app/build.rs b/crates/os-app/build.rs index cdb125eb9..39d1a7379 100644 --- a/crates/os-app/build.rs +++ b/crates/os-app/build.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-or-later // This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio -//! Embeds `assets/lemonstudio.ico` as icon resource 1 (Windows). Uses the Windows SDK's `rc.exe` -//! (or `$RC`); without it the build goes on and the exe just has no icon. Also sets `cfg(plugin_windows)` where plugin -//! windows exist. +//! Embeds `assets/lemonstudio.ico` (a nightly's: `lemonstudio-nightly.ico`) as icon resource 1 and the version +//! (`LEMON_VERSION`, else the workspace's) as the version resource (Windows). Uses the Windows SDK's `rc.exe` (or `$RC`); +//! without it the build goes on and the exe just has no icon. Also sets `cfg(plugin_windows)` where plugin windows exist, +//! `cfg(lemon_updater)` when `LEMON_UPDATER` names the updater to carry (release builds, `crate::update`), and +//! `cfg(lemon_nightly)` when `LEMON_CHANNEL` is `nightly` (`crate::version`). use std::path::PathBuf; use std::process::Command; @@ -57,8 +59,81 @@ fn factory_presets() { std::fs::write(PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("factory_presets.rs"), out).unwrap(); } +/// Release builds: `LEMON_UPDATER` names the `lemonstudio-updater` built just before, which the program carries +/// (`update::UPDATER`, through `LEMON_UPDATER_FILE`). A build from source has none. +fn updater() { + println!("cargo::rustc-check-cfg=cfg(lemon_updater)"); + println!("cargo:rerun-if-env-changed=LEMON_UPDATER"); + let Some(path) = std::env::var_os("LEMON_UPDATER").filter(|p| !p.is_empty()) else { return }; + let path = std::path::absolute(PathBuf::from(path)).expect("LEMON_UPDATER"); + assert!(path.is_file(), "LEMON_UPDATER names no file: {}", path.display()); + println!("cargo:rerun-if-changed={}", path.display()); + println!("cargo::rustc-env=LEMON_UPDATER_FILE={}", path.display()); + println!("cargo::rustc-cfg=lemon_updater"); +} + +/// The program's version: the release workflow's `LEMON_VERSION`, else the workspace's (`crate::version`). +fn version() -> String { + println!("cargo:rerun-if-env-changed=LEMON_VERSION"); + std::env::var("LEMON_VERSION").ok().filter(|v| !v.is_empty()).unwrap_or_else(|| std::env::var("CARGO_PKG_VERSION").unwrap()) +} + +/// Whether this is a nightly build (`crate::version::NIGHTLY`). +fn nightly() -> bool { + println!("cargo::rustc-check-cfg=cfg(lemon_nightly)"); + println!("cargo:rerun-if-env-changed=LEMON_CHANNEL"); + let nightly = std::env::var("LEMON_CHANNEL").is_ok_and(|c| c == "nightly"); + if nightly { + println!("cargo::rustc-cfg=lemon_nightly"); + } + nightly +} + +/// Windows: the resource script, the icon and the version resource (four numbers: a nightly's `YEAR.MONTH.DAY.N` +/// fits as it is, a semantic version gets a 0 at the end). `name` is the program's (`crate::version::NAME`). +fn resource_script(ico: &std::path::Path, version: &str, name: &str) -> String { + let mut n: Vec = version.split(['-', '+']).next().unwrap_or("").split('.').map(|p| p.parse().unwrap_or(0)).collect(); + n.resize(4, 0); + let nums = format!("{},{},{},{}", n[0], n[1], n[2], n[3]); + let ico = ico.display().to_string().replace('\\', "\\\\"); + let text = version.replace('"', ""); + format!( + r#"1 ICON "{ico}" + +1 VERSIONINFO +FILEVERSION {nums} +PRODUCTVERSION {nums} +FILEOS 0x40004 +FILETYPE 0x1 +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904B0" + BEGIN + VALUE "CompanyName", "The Lemon Studio contributors" + VALUE "FileDescription", "{name}" + VALUE "FileVersion", "{text}" + VALUE "InternalName", "lemonstudio" + VALUE "LegalCopyright", "Copyright (C) 2026 ItsAlphaNeon and the Lemon Studio contributors. GPL-3.0-or-later." + VALUE "OriginalFilename", "lemonstudio.exe" + VALUE "ProductName", "{name}" + VALUE "ProductVersion", "{text}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END +"# + ) +} + fn main() { factory_presets(); + updater(); + let version = version(); + let nightly = nightly(); // `plugin_windows`: plugin editors open in Lemon Studio's frame windows (Win32 on Windows, X11 on Linux, AppKit on // macOS). println!("cargo::rustc-check-cfg=cfg(plugin_windows)"); @@ -67,6 +142,7 @@ fn main() { } println!("cargo:rerun-if-changed=assets/lemonstudio.rc"); println!("cargo:rerun-if-changed=assets/lemonstudio.ico"); + println!("cargo:rerun-if-changed=assets/lemonstudio-nightly.ico"); if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("windows") || std::env::var("CARGO_CFG_TARGET_ENV").as_deref() != Ok("msvc") { @@ -76,11 +152,16 @@ fn main() { println!("cargo:warning=rc.exe not found (Windows SDK); building without the app icon"); return; }; - let out = PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("lemonstudio.res"); + let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let out = out_dir.join("lemonstudio.res"); + let (ico, name) = if nightly { ("assets/lemonstudio-nightly.ico", "Lemon Studio Nightly") } else { ("assets/lemonstudio.ico", "Lemon Studio") }; + let ico = std::path::absolute(ico).expect("icon path"); + let script = out_dir.join("lemonstudio.rc"); + std::fs::write(&script, resource_script(&ico, &version, name)).expect("resource script"); let ok = Command::new(&rc) .args(["/nologo", "/fo"]) .arg(&out) - .arg("assets/lemonstudio.rc") + .arg(&script) .status() .is_ok_and(|s| s.success()); if ok { diff --git a/crates/os-app/src/app.rs b/crates/os-app/src/app.rs index cfd1c8e67..219dda1a1 100644 --- a/crates/os-app/src/app.rs +++ b/crates/os-app/src/app.rs @@ -1432,6 +1432,8 @@ pub struct App { pub(crate) track_rec: Option, /// TOOLS > Retro audio capture: the last minutes the device played (spec: ui/audio-logger.md). pub(crate) audio_log: crate::audio_log::Log, + /// Checking for, downloading and installing a newer build (`crate::update`). + pub(crate) updates: crate::update::Updates, /// Wheel movement in an open menu short of a whole notch (trackpads). menu_notch: f32, /// A Shift+press grabbed piano-roll notes: the drag's first move picks up copies of them (`App::pick_up_notes`). @@ -1698,6 +1700,7 @@ impl App { audio_rec: None, track_rec: None, audio_log: Default::default(), + updates: Default::default(), menu_notch: 0.0, shift_clone: false, auto_close: Default::default(), @@ -2208,6 +2211,9 @@ impl App { if n == crate::dialog::Next::Quit { self.quit_answer = Some(false); } + if matches!(n, crate::dialog::Next::Quit | crate::dialog::Next::Close) { + self.update_put_off(); + } } /// A dialog was answered: what waited for it (spec: ui/dialogs.md §5). @@ -2383,6 +2389,11 @@ impl App { self.open_marker_settings(); } } + Then::Update(found) => { + if let Reply::Button(a) = reply { + self.update_answer(found, a); + } + } Then::MidiExportConfirm => { if reply == Reply::Button(Answer::Ok) { self.state.message = Some(self.prepare_midi_export()); @@ -4286,6 +4297,7 @@ impl App { } self.record_tick(); self.audio_log.tick(); + self.update_tick(); // CPU panel (spec: ui/main-window.md §13) and the Plugin performance monitor: a reading at the panel's refresh // rate. let now = std::time::Instant::now(); @@ -5756,6 +5768,7 @@ impl App { } SAct::Button("Load theme...") => self.theme_dialog(), SAct::Button(views::settings::SHOW_ASIO_PANEL) => self.transport.asio_panel(), + SAct::Button(views::settings::CHECK_UPDATES) => self.check_updates_now(), SAct::Button("Manage plugins") => self.open_settings(views::settings::PLUGINS), SAct::Button("Manage messages") => return self.messages_menu(), SAct::Button("Scan for new plugins") => self.start_scan(crate::plugin_db::Mode::Changed), @@ -9523,7 +9536,7 @@ impl App { (HELP_MENU, HELP_ABOUT) => { // The Windows build includes the ASIO SDK, which is under version 3 only (THIRD-PARTY-NOTICES.txt §9). let licence = if cfg!(windows) { "the GNU GPL v3" } else { "the GNU GPL v3 or later" }; - self.state.message = Some(format!("Lemon Studio {} (pre-alpha), free software under {licence}", env!("CARGO_PKG_VERSION"))); + self.state.message = Some(format!("Lemon Studio {} (pre-alpha), free software under {licence}", crate::version::text())); return; } (PATTERNS, i) if i >= PATTERN_PICK => return crate::patterns::select(&mut self.state, (i - PATTERN_PICK) as u16), @@ -9566,6 +9579,7 @@ impl App { MenuCmd::SaveMidiClip => self.save_midi_clipboard(), MenuCmd::SaveAudioClip => self.save_audio_clipboard(), MenuCmd::Diagnostics => self.save_diagnostics(), + MenuCmd::CheckUpdates => self.check_updates_now(), MenuCmd::NewPattern => { let at = crate::patterns::box_at_mouse(self.mouse); if let Some(m) = crate::patterns::run(&mut self.state, crate::patterns::Cmd::FindFirst, at) { @@ -9929,7 +9943,7 @@ fn lemon_or_flp(file: &mut os_flp::FlpFile, target: &std::path::Path, mut extra: os_flp::lemon::to_flp(file); return Ok(()); } - extra.insert(0, os_flp::lemon::saved_by(&format!("Lemon Studio {}", env!("CARGO_PKG_VERSION")))); + extra.insert(0, os_flp::lemon::saved_by(&format!("Lemon Studio {}", crate::version::VERSION))); os_flp::lemon::to_lemon(file, &extra).map_err(|e| e.to_string()) } @@ -16162,7 +16176,7 @@ mod tests { assert_eq!(std::fs::read(&flp).unwrap(), before, "the FLP is still not written"); let file = os_flp::FlpFile::parse(&std::fs::read(&lemon).unwrap()).unwrap(); assert_eq!(os_flp::lemon::version(&file), Some(os_flp::lemon::VERSION)); - assert_eq!(os_flp::lemon::saved_by_text(&file), Some(format!("Lemon Studio {}", env!("CARGO_PKG_VERSION")))); + assert_eq!(os_flp::lemon::saved_by_text(&file), Some(format!("Lemon Studio {}", crate::version::VERSION))); // Save again: in place, no dialog. app.state.tempo = 125.0; app.act(Action::MenuItem(0, views::menus::FILE_SAVE)); diff --git a/crates/os-app/src/control/rpc.rs b/crates/os-app/src/control/rpc.rs index ade898db8..118d24bff 100644 --- a/crates/os-app/src/control/rpc.rs +++ b/crates/os-app/src/control/rpc.rs @@ -121,7 +121,7 @@ impl App { if let Some(n) = params.get("name").and_then(Value::as_str).map(str::trim).filter(|n| !n.is_empty()) { c.name = n.chars().take(40).collect(); } - Ok(json!({ "app": "Lemon Studio", "version": env!("CARGO_PKG_VERSION"), "protocol": PROTOCOL, "name": c.name, "level": c.level })) + Ok(json!({ "app": "Lemon Studio", "version": crate::version::VERSION, "protocol": PROTOCOL, "name": c.name, "level": c.level })) } "state" => Ok(super::query::project(&self.state)), "notes" => super::query::notes(&self.state, uint(params, "pattern")? as u32).map_err(bad), diff --git a/crates/os-app/src/diagnostics.rs b/crates/os-app/src/diagnostics.rs index f10f22266..5726529fd 100644 --- a/crates/os-app/src/diagnostics.rs +++ b/crates/os-app/src/diagnostics.rs @@ -29,7 +29,7 @@ impl App { let cfg = crate::audio_cfg::AudioCfg::current(); let cpus = std::thread::available_parallelism().map_or(1, |n| n.get()); let out = [ - format!("Lemon Studio {} (pre-alpha)", env!("CARGO_PKG_VERSION")), + format!("Lemon Studio {}", crate::version::staged()), format!("System: {} {}, {cpus} logical processors", std::env::consts::OS, std::env::consts::ARCH), format!("User folder: {}", os_paths::user_dir().map_or_else(|| "none".into(), |d| d.display().to_string())), String::new(), @@ -84,7 +84,7 @@ mod tests { let path = super::report_path().unwrap(); assert_eq!(app.state.message, Some(format!("Saved {}", path.display()))); let text = std::fs::read_to_string(&path).unwrap(); - assert!(text.starts_with(&format!("Lemon Studio {} (pre-alpha)\n", env!("CARGO_PKG_VERSION"))), "{text}"); + assert!(text.starts_with(&format!("Lemon Studio {}\n", crate::version::staged())), "{text}"); assert!(text.contains(&format!("System: {} {}", std::env::consts::OS, std::env::consts::ARCH)), "{text}"); assert!(text.contains("Project: not saved\n Channels: 5, patterns: 1, mixer tracks: 18"), "{text}"); let _ = std::fs::remove_file(path); diff --git a/crates/os-app/src/dialog.rs b/crates/os-app/src/dialog.rs index d0ea71194..e878c9086 100644 --- a/crates/os-app/src/dialog.rs +++ b/crates/os-app/src/dialog.rs @@ -137,6 +137,8 @@ pub enum Then { MidiExportConfirm, /// Marker settings: Accept (Ok) prepares with this count and spacing. PerfMarkers(u8, u8), + /// The update offered (`crate::update`): Install (Yes), Skip this version (No) or Later (Cancel). + Update(crate::update::Found), } /// What waits for the save-changes question. diff --git a/crates/os-app/src/file_assoc.rs b/crates/os-app/src/file_assoc.rs index 7479f86bf..658230d3f 100644 --- a/crates/os-app/src/file_assoc.rs +++ b/crates/os-app/src/file_assoc.rs @@ -22,7 +22,7 @@ pub const EXT: &str = ".flp"; pub const LEMON_EXT: &str = ".lemon"; /// The type's description and the open verb's name (spec). const DESCRIPTION: &str = "Lemon Studio project"; -const FRIENDLY_NAME: &str = "Lemon Studio"; +const FRIENDLY_NAME: &str = crate::version::NAME; /// The per-user classes, where every write goes (`Root::Classes`). const CLASSES: &str = r"Software\Classes"; /// Windows' "Open with ▸ Always" choice for `.flp` (`Root::User`), value `ProgId`. diff --git a/crates/os-app/src/logo.rs b/crates/os-app/src/logo.rs index 7c5ce1f1d..b92f70b08 100644 --- a/crates/os-app/src/logo.rs +++ b/crates/os-app/src/logo.rs @@ -22,5 +22,11 @@ pub const SLICES: [&str; 4] = [ "M 423.113 447.512 C 405.969 432.219 391.163 419.069 390.286 418.289 C 383.857 412.64 370.999 401.145 370.609 400.756 C 370.317 400.464 366.226 396.762 361.356 392.476 C 356.583 388.19 351.907 384.002 351.128 383.222 C 350.349 382.443 347.719 380.008 345.283 377.962 C 342.848 375.819 337.198 370.754 332.718 366.76 C 323.269 358.286 314.308 350.298 308.755 345.428 C 306.612 343.48 302.619 339.973 299.989 337.538 C 297.261 335.103 293.267 331.499 291.222 329.648 C 289.079 327.7 272.909 313.186 255.181 297.406 C 237.55 281.626 220.698 266.82 217.971 264.677 C 211.055 259.222 204.431 257.079 194.788 257.176 C 180.566 257.176 173.65 261.852 162.448 279.19 C 149.298 299.354 139.46 318.349 128.842 344.259 C 122.316 360.039 114.523 384.976 110.627 402.704 C 109.945 405.626 109.068 409.815 108.581 411.958 C 108.094 414.101 107.315 418.484 106.731 421.699 C 102.25 448.388 101.178 459.883 102.64 466.506 C 105.27 478.098 111.796 486.085 122.803 491.345 C 129.329 494.56 129.622 494.56 142.674 494.463 C 149.883 494.365 162.156 493.878 169.949 493.391 C 177.741 492.807 192.645 491.93 203.067 491.345 C 213.49 490.761 229.562 489.884 238.621 489.397 C 257.421 488.326 298.82 485.988 326.289 484.527 C 336.517 483.942 352.005 483.066 360.869 482.579 C 369.733 482.092 385.318 481.215 395.449 480.631 C 405.676 480.046 420.093 479.169 427.593 478.682 C 435.094 478.098 444.153 477.708 447.854 477.708 C 452.432 477.708 454.381 477.319 454.283 476.539 C 454.283 475.858 440.256 462.805 423.113 447.512 Z", ]; +// The Nightly logo's own parts (`logo-colored-lemon-nightly.svg`): the moon's craters on the label, the record +// without its two windows, and the stars on it. The rest it shares with the full-colour logo. +pub const NIGHTLY_CRATERS: &str = "M 547 398 C 547 411.807 558.193 423 572 423 C 585.807 423 597 411.807 597 398 C 597 384.193 585.807 373 572 373 C 558.193 373 547 384.193 547 398 Z M 600 492 C 600 500.284 606.716 507 615 507 C 623.284 507 630 500.284 630 492 C 630 483.716 623.284 477 615 477 C 606.716 477 600 483.716 600 492 Z M 485 366 C 485 372.627 490.373 378 497 378 C 503.627 378 509 372.627 509 366 C 509 359.373 503.627 354 497 354 C 490.373 354 485 359.373 485 366 Z"; +pub const NIGHTLY_RECORD: &str = "M 966.748 458.714 C 964.702 424.036 960.124 395.691 951.065 360.916 C 947.85 348.545 942.006 329.94 939.863 325.07 C 939.084 323.414 938.499 321.66 938.499 321.173 C 938.499 317.569 914.05 263.8 908.303 254.644 C 907.816 253.864 905.77 250.358 903.822 246.851 C 897.003 234.967 883.464 214.317 874.307 202.043 C 849.858 169.217 815.473 134.15 783.62 109.505 C 755.567 87.881 715.727 63.918 688.16 52.132 C 682.803 49.892 678.322 47.846 670.14 44.242 C 667.704 43.17 665.172 42.294 664.49 42.294 C 663.711 42.294 662.931 42.001 662.542 41.709 C 661.763 40.833 648.71 36.254 631.176 30.605 C 620.462 27.195 590.46 20.085 577.602 17.942 C 570.686 16.773 564.744 15.701 564.452 15.506 C 564.16 15.409 561.14 14.922 557.633 14.63 C 554.127 14.24 545.36 13.266 538.152 12.487 C 492.272 7.616 437.334 11.026 389.117 21.838 C 370.22 26.027 333.887 36.741 326.289 40.346 C 325.217 40.93 321.321 42.391 317.522 43.852 C 313.821 45.216 308.366 47.456 305.541 48.723 C 302.716 49.989 300.183 51.06 299.989 51.06 C 299.404 51.06 268.526 66.256 263.948 68.886 C 257.908 72.295 246.901 78.627 244.368 80.283 C 232.387 87.881 222.939 94.115 217.679 97.914 C 214.172 100.446 209.399 104.05 206.964 105.804 C 194.398 114.96 177.157 129.961 160.11 146.52 L 147.545 158.696 L 251.479 251.916 C 308.755 303.153 360.284 349.324 366.031 354.584 L 376.551 364.033 L 380.643 360.137 C 384.831 356.24 391.357 350.98 396.325 347.668 C 397.884 346.597 400.124 345.038 401.293 344.259 C 407.04 340.363 417.658 334.713 425.645 331.304 C 466.459 313.576 512.436 312.699 553.347 328.771 C 570.199 335.298 579.647 340.655 594.454 351.954 C 615.689 368.124 630.982 386.437 642.768 410.204 C 663.516 451.895 666.146 498.943 650.171 543.459 C 646.177 554.758 635.365 574.824 628.059 584.663 L 625.819 587.585 L 738.033 688.207 C 799.79 743.633 851.319 789.707 852.585 790.778 C 854.826 792.629 854.826 792.532 857.748 789.317 C 859.306 787.466 862.034 784.057 863.69 781.622 C 865.443 779.187 869.047 774.414 871.872 770.907 C 877.424 763.894 893.984 739.639 898.854 731.262 C 900.607 728.34 902.945 724.346 904.114 722.398 C 920.868 693.955 934.7 661.615 948.24 619.437 C 955.643 596.352 963.046 556.609 965.774 525.438 C 967.137 510.437 967.624 474.396 966.748 458.714 Z"; +pub const NIGHTLY_STARS: &str = "M 762 350 Q 762 402 814 402 Q 762 402 762 454 Q 762 402 710 402 Q 762 402 762 350 Z M 868 518 Q 868 548 898 548 Q 868 548 868 578 Q 868 548 838 548 Q 868 548 868 518 Z M 742 582 Q 742 604 764 604 Q 742 604 742 626 Q 742 604 720 604 Q 742 604 742 582 Z M 612 132 Q 612 172 652 172 Q 612 172 612 212 Q 612 172 572 172 Q 612 172 612 132 Z M 425 98 Q 425 122 449 122 Q 425 122 425 146 Q 425 122 401 122 Q 425 122 425 98 Z M 790 236 Q 790 262 816 262 Q 790 262 790 288 Q 790 262 764 262 Q 790 262 790 236 Z M 302 148 Q 302 168 322 168 Q 302 168 302 188 Q 302 168 282 168 Q 302 168 302 148 Z"; + /// The greyscale mark, drawn in one tint (`icons::logo`). pub const PATHS: &[&str] = &[RECORD, CENTER, RIND, SLICES[0], SLICES[1], SLICES[2], SLICES[3]]; diff --git a/crates/os-app/src/main.rs b/crates/os-app/src/main.rs index adf14573d..f7fe46489 100644 --- a/crates/os-app/src/main.rs +++ b/crates/os-app/src/main.rs @@ -134,6 +134,8 @@ mod save_form; mod scroll_speed; mod search_folders; mod settings_file; +mod update; +mod version; mod shortcuts; mod smooth; mod snap; @@ -712,7 +714,7 @@ impl ApplicationHandler for Shell { } splash::step(&self.splash, splash::WINDOW, "Opening the main window"); let attrs = Window::default_attributes() - .with_title("Lemon Studio") + .with_title(version::NAME) .with_decorations(false) .with_inner_size(LogicalSize::new(1920.0, 1080.0)) // Layouts are measured at 1920x1080 and are not responsive yet; below this they break. @@ -745,7 +747,7 @@ impl ApplicationHandler for Shell { .with_maximized(m.max), None => attrs, }; - // App icon: resource 1, embedded by build.rs from assets/lemonstudio.ico. + // App icon: resource 1, embedded by build.rs from assets/lemonstudio.ico (a nightly's: lemonstudio-nightly.ico). #[cfg(windows)] let attrs = { use winit::dpi::PhysicalSize; @@ -832,6 +834,8 @@ impl ApplicationHandler for Shell { /// (`macos::answer_quit`) ends the process from AppKit, after this: the plugin windows close and the instances go as /// they do when `run_app` returns. fn exiting(&mut self, _el: &ActiveEventLoop) { + // An update to install: the updater waits for this process to end (`update`). + self.app.updates.on_exit(); if let Some(window) = self.window.clone() { self.follow_frame(&window); } @@ -1338,15 +1342,17 @@ impl ApplicationHandler for Shell { } } -/// The application id on Linux: Wayland's app id and X11's WM_CLASS, matching `assets/lemonstudio.desktop`. +/// The application id on Linux: Wayland's app id and X11's WM_CLASS, matching `assets/lemonstudio.desktop` (a nightly's +/// AppImage: `lemonstudio-nightly.desktop`, `tools/package/appimage.sh`). #[cfg(all(unix, not(target_os = "macos")))] -const APP_ID: &str = "lemonstudio"; +const APP_ID: &str = if version::NIGHTLY { "lemonstudio-nightly" } else { "lemonstudio" }; -/// The app icon as RGBA bytes, width and height: `assets/lemonstudio.png`, 256 px (X11 windows show it; Wayland takes the -/// desktop entry's). +/// The app icon as RGBA bytes, width and height: `assets/lemonstudio.png` (a nightly's: `lemonstudio-nightly.png`), 256 px +/// (X11 windows show it; Wayland takes the desktop entry's). #[cfg(all(unix, not(target_os = "macos")))] pub(crate) fn icon_rgba() -> Option<(Vec, u32, u32)> { - let png = tiny_skia::Pixmap::decode_png(include_bytes!("../assets/lemonstudio.png")).ok()?; + let file: &[u8] = if version::NIGHTLY { include_bytes!("../assets/lemonstudio-nightly.png") } else { include_bytes!("../assets/lemonstudio.png") }; + let png = tiny_skia::Pixmap::decode_png(file).ok()?; let rgba = png.pixels().iter().flat_map(|p| { let c = p.demultiply(); [c.red(), c.green(), c.blue(), c.alpha()] @@ -1979,6 +1985,8 @@ fn main() { } // The windows where the last run left them. layout::restore(&mut app.state); + // How the last update went, and the check for a newer build (release builds; `update`). + app.update_startup(); // MIDI devices (spec: ui/midi-settings.md §2): the enabled inputs play the selected channel; master sync goes out. splash::step(&splash, splash::MIDI, "Opening MIDI devices"); app.midi = midi_in::MidiIn::open(wake_ui); diff --git a/crates/os-app/src/panic_log.rs b/crates/os-app/src/panic_log.rs index 541218c91..b5c68e4a7 100644 --- a/crates/os-app/src/panic_log.rs +++ b/crates/os-app/src/panic_log.rs @@ -33,7 +33,7 @@ fn entry(info: &std::panic::PanicHookInfo) -> String { format!( "{} Lemon Studio {} panicked on thread {} at {}: {}\n{}\n\n", chrono::Local::now().format("%Y-%m-%d %H:%M:%S"), - env!("CARGO_PKG_VERSION"), + crate::version::text(), thread.name().unwrap_or("(unnamed)"), at, message(info.payload()), diff --git a/crates/os-app/src/prefs.rs b/crates/os-app/src/prefs.rs index acdc91f87..0e15da220 100644 --- a/crates/os-app/src/prefs.rs +++ b/crates/os-app/src/prefs.rs @@ -255,6 +255,15 @@ pub const SHOW_SPLASH: Check = Check { key: "show_startup_splash", label: "Show /// gets one, this check silences it. Spec: ui/settings-choices.md "Miscellaneous: naming, detached plugins and startup". pub const SILENT_STARTUP: Check = Check { key: "silent_startup", label: "Silent startup", default: false }; +/// General ▸ Updates: at start, a release build checks its channel (nightly or stable) for a newer build +/// (`crate::update`). A build from source never checks. +pub const AUTO_CHECK_UPDATES: Check = Check { key: "auto_check_updates", label: "Automatically check for updates", default: true }; + +/// General ▸ Updates: a newer build found is downloaded right away, so Install and restart has nothing to wait +/// for; off, it is downloaded once Install is chosen. +pub const AUTO_DOWNLOAD_UPDATES: Check = + Check { key: "auto_download_updates", label: "Automatically download program updates", default: true }; + /// General ▸ Input: off, a left or a right click on a step of the step sequencer toggles it; on, the left button only /// sets steps and the right one only clears them. A drag sets (left) or clears (right) the steps it crosses either way /// (`App::rack_step`). Spec: ui/settings-choices.md "Input: Click-and-hold and both mouse buttons (2026-10-16)". @@ -588,6 +597,8 @@ pub const CHECKS: &[&Check] = &[ &LIMIT_EDITORS, &SHOW_SPLASH, &SILENT_STARTUP, + &AUTO_CHECK_UPDATES, + &AUTO_DOWNLOAD_UPDATES, &BOTH_MOUSE_BUTTONS, &CLICK_AND_HOLD, &UNDO_KNOB_TWEAKS, @@ -699,10 +710,10 @@ fn slider_in(st: &Store, s: &Slider) -> u16 { st.get(s.key).and_then(|v| v.parse::().ok()).map_or(s.default, |v| v.min(s.max)) } -/// Tests only: a check that exercises the settings page. It stays greyed for real until Lemon Studio can update itself -/// (ui/options-coverage.md §4). +/// Tests only: a check that exercises the settings page, on a control that stays greyed for real (General ▸ Advanced; +/// ui/options-coverage.md §4). #[cfg(test)] -pub const TEST_CHECK: Check = Check { key: "test_check", label: "Automatically check for updates", default: true }; +pub const TEST_CHECK: Check = Check { key: "test_check", label: "Use the high-performance power plan", default: true }; fn checks() -> impl Iterator { #[cfg(test)] @@ -911,7 +922,7 @@ main_gui_scaling = 200% set_choice(&MAIN_GUI_SCALING, 99); assert_eq!(choice(&MAIN_GUI_SCALING), "125%"); assert_eq!(find_combo("Main GUI scaling"), Some(&MAIN_GUI_SCALING)); - assert_eq!(find_check("Automatically check for updates"), Some(&TEST_CHECK)); + assert_eq!(find_check("Use the high-performance power plan"), Some(&TEST_CHECK)); assert_eq!((find_check("Main GUI scaling"), find_combo("Language"), find_check("")), (None, None, None)); } @@ -1019,21 +1030,23 @@ main_gui_scaling = 200% click(&mut app, &mut c, r); assert_eq!(app.state.playlist.popup.as_ref().unwrap().items[2].mark, crate::playlist::Mark::Radio(true)); app.state.playlist.popup = None; - // The test check (Updates, further down the page: the groups above it folded) toggles. - app.state.settings.folded.extend(["Display", "Language", "Input", "Undo history"]); + // The test check (Advanced, further down the page: the groups above it folded) toggles. + app.state.settings.folded.extend(["Display", "Language", "Input", "Undo history", "Hints and warnings", "System", "Updates", "Miscellaneous"]); app.paint(&mut c, None); let key = Action::Settings(SAct::Check(TEST_CHECK.label)); let r = hit_rect(&app, &key); click(&mut app, &mut c, r); assert!(!check(&TEST_CHECK)); - assert_eq!(app.state.message.as_deref(), Some("Automatically check for updates: off")); + assert_eq!(app.state.message.as_deref(), Some("Use the high-performance power plan: off")); let r = hit_rect(&app, &key); click(&mut app, &mut c, r); assert!(check(&TEST_CHECK)); - // Its neighbours are not declared: greyed out. - let download = "Automatically download program updates"; - assert!(app.hits.iter().any(|h| h.action == Action::Placeholder(download))); - assert!(!app.hits.iter().any(|h| h.action == Action::Settings(SAct::Check(download)))); + // The update checks above it are real settings (`crate::update`), not greyed out. + app.state.settings.folded.retain(|g| *g != "Updates"); + app.paint(&mut c, None); + let download = AUTO_DOWNLOAD_UPDATES.label; + assert!(app.hits.iter().any(|h| h.action == Action::Settings(SAct::Check(download)))); + assert!(!app.hits.iter().any(|h| h.action == Action::Placeholder(download))); // Nothing about usage data. assert!(!app.hits.iter().any(|h| matches!(h.action, Action::Placeholder(l) if l.contains("usage")))); } diff --git a/crates/os-app/src/splash.rs b/crates/os-app/src/splash.rs index a9499eb2d..7d0268d7a 100644 --- a/crates/os-app/src/splash.rs +++ b/crates/os-app/src/splash.rs @@ -8,7 +8,8 @@ //! round the record's grooves, and a status line under the disc names the step. Once the main window shows and the //! opened project has loaded (and the splash has shown for a moment, however quick that was), the lemon tops up, the //! light fades, the mark pops and the splash fades away. With the check off there is none. A program setting, read -//! once at start (`main`); `--screenshot`, the helper processes and tests never open it. +//! once at start (`main`); `--screenshot`, the helper processes and tests never open it. A nightly build shows the +//! Nightly logo. //! //! The window has no background of its own: only the disc, its soft (Gaussian) shadow and the status line show, over //! whatever is behind, and clicks go through it. It is animated on a thread of its own (on macOS in a process of its @@ -56,14 +57,51 @@ pub const LOADED: f32 = 0.96; // concentric). const C: (f32, f32) = (490.4, 488.1); const R: f32 = 476.5; -// The full-colour logo's fills (`assets/logo/logo-colored-lemon.svg`): the label and the rind, the slices from the -// lower right round to the upper left, the record's edge and its gradient (lower right to upper left). +// The logo's fills: the rind and the slices from the lower right round to the upper left, and the line of the record's +// gradient (lower right to upper left), the same in every logo. const LEMON: u32 = 0xffe066; const SLICE_FILLS: [u32; 4] = [0xffe066, 0xffe88a, 0xffeda3, 0xfff1b8]; -const RECORD_EDGE: u32 = 0x2d2d2d; const RECORD_FROM: (f32, f32) = (857.748, 789.317); const RECORD_TO: (f32, f32) = (147.545, 158.696); -const RECORD_STOPS: [(f32, u32); 3] = [(0.0, 0x2d2d2d), (0.387, 0x171717), (0.8914, 0x000000)]; + +/// The record half of a logo, the part that differs between them. +struct Face { + /// The label's fill. + label: u32, + /// Over the label, under the record (path data and fill). + under: (&'static str, u32), + /// The record's shape, its edge and its gradient. + record: &'static str, + edge: u32, + stops: [(f32, u32); 3], + /// Over the record (path data and fill). + over: Option<(&'static str, u32)>, + /// The spindle hole's fill. + center: u32, +} + +/// `assets/logo/logo-colored-lemon.svg`: the shimmer seen through the record's two windows. +const FACE: Face = Face { + label: LEMON, + under: (logo::SHIMMER, 0xffffff), + record: logo::RECORD, + edge: 0x2d2d2d, + stops: [(0.0, 0x2d2d2d), (0.387, 0x171717), (0.8914, 0x000000)], + over: None, + center: 0x000000, +}; + +/// `assets/logo/logo-colored-lemon-nightly.svg`: a moon with its craters rising over the wedge's edge, a night sky for +/// the record and stars on it. +const NIGHTLY_FACE: Face = Face { + label: 0xf6f2df, + under: (logo::NIGHTLY_CRATERS, 0xd9d4ba), + record: logo::NIGHTLY_RECORD, + edge: 0x2f3a8a, + stops: [(0.0, 0x2f3a8a), (0.387, 0x1a2056), (0.8914, 0x0c0f2e)], + over: Some((logo::NIGHTLY_STARS, 0xfff4c2)), + center: 0x0c0f2e, +}; /// The empty part of the gauge: each fill this far toward the workspace grey. const EMPTY: (u32, f32) = (0x262626, 0.8); /// The pie sector the gauge fills, clockwise from the rind's lower end (degrees clockwise from +x). @@ -313,8 +351,9 @@ fn device(scale: f32) -> (u32, u32) { /// a frame only fills. struct Parts { label: Path, - shimmer: Path, + under: Path, record: Path, + over: Option, record_edge: Path, center: Path, rind: Path, @@ -323,17 +362,22 @@ struct Parts { } impl Parts { - fn new() -> Option { + fn new(face: &Face) -> Option { let p = |d: &str| os_ui::path_data(d, 0.0, 0.0, 1.0); // Outlines `w` logo units wide, flattened finely enough for the largest splash. let outline = |path: &Path, w: f32| path.stroke(&Stroke { width: w, ..Stroke::default() }, 2.0); let [s1, s2, s3, s4] = logo::SLICES; - let (record, rind) = (p(logo::RECORD)?, p(logo::RIND)?); + let (record, rind) = (p(face.record)?, p(logo::RIND)?); + let over = match face.over { + Some((d, _)) => Some(p(d)?), + None => None, + }; Some(Parts { label: p(logo::LABEL)?, - shimmer: p(logo::SHIMMER)?, + under: p(face.under.0)?, record_edge: outline(&record, 1.0)?, record, + over, center: p(logo::CENTER)?, rind_edge: outline(&rind, 1.0)?, rind, @@ -403,10 +447,16 @@ struct Painter { record_mask: Mask, record_scale: f32, parts: Parts, + face: &'static Face, } impl Painter { + /// The painter of this build's logo. fn new(scale: f32) -> Option { + Painter::with_face(scale, if crate::version::NIGHTLY { &NIGHTLY_FACE } else { &FACE }) + } + + fn with_face(scale: f32, face: &'static Face) -> Option { let (w, h) = device(scale); let caption = Canvas::new((DISC.2 * scale).round() as u32, (CAPTION_H * scale).round() as u32, scale).ok()?; Some(Painter { @@ -418,7 +468,8 @@ impl Painter { mask: Mask::new(w, h)?, record_mask: Mask::new(w, h)?, record_scale: f32::NAN, - parts: Parts::new()?, + parts: Parts::new(face)?, + face, }) } @@ -441,12 +492,17 @@ impl Painter { let k = DISC.2 * s * look.scale / (2.0 * R); let t = Transform::from_translate(cx, cy).pre_scale(k, k).pre_translate(-C.0, -C.1); let fill = |px: &mut Pixmap, path: &Path, c: u32, mask: Option<&Mask>| px.fill_path(path, &solid(c), FillRule::Winding, t, mask); - // The record half: the label and the shimmer seen through its cut-outs, the record, the spindle. - fill(px, &p.label, LEMON, None); - fill(px, &p.shimmer, 0xffffff, None); - px.fill_path(&p.record, &record_paint(), FillRule::Winding, t, None); - fill(px, &p.record_edge, RECORD_EDGE, None); - fill(px, &p.center, 0x000000, None); + // The record half: the label and what is on it seen through its cut-outs, the record, what is on that, the + // spindle. + let face = self.face; + fill(px, &p.label, face.label, None); + fill(px, &p.under, face.under.1, None); + px.fill_path(&p.record, &record_paint(face), FillRule::Winding, t, None); + if let (Some(over), Some((_, c))) = (&p.over, face.over) { + fill(px, over, c, None); + } + fill(px, &p.record_edge, face.edge, None); + fill(px, &p.center, face.center, None); // The groove light: its two wedges and the grooves inside them, on the record only, brightest along the // wedges' middle. if look.light > 0.001 { @@ -566,8 +622,8 @@ fn mix(a: u32, b: u32, k: f32) -> u32 { } /// The record's gradient fill. -fn record_paint() -> Paint<'static> { - let stops = RECORD_STOPS.iter().map(|&(at, c)| GradientStop::new(at, sk(c, 1.0))).collect(); +fn record_paint(face: &Face) -> Paint<'static> { + let stops = face.stops.iter().map(|&(at, c)| GradientStop::new(at, sk(c, 1.0))).collect(); let mut p = Paint::default(); if let Some(shader) = LinearGradient::new(Point::from_xy(RECORD_FROM.0, RECORD_FROM.1), Point::from_xy(RECORD_TO.0, RECORD_TO.1), stops, SpreadMode::Pad, Transform::identity()) { p.shader = shader; @@ -718,7 +774,7 @@ mod win { wc.hInstance = inst; wc.lpszClassName = class.as_ptr(); RegisterClassW(&wc); - let title = wide("Lemon Studio"); + let title = wide(crate::version::NAME); // Layered (per-pixel alpha) and transparent to the mouse; above every window that is not topmost too; a // tool window takes no taskbar button; never activated, so the main window gets the focus as before. let hwnd = CreateWindowExW( @@ -912,7 +968,7 @@ mod x11 { if let (Some(ty), Some(splash)) = (atom(b"_NET_WM_WINDOW_TYPE"), atom(b"_NET_WM_WINDOW_TYPE_SPLASH")) { let _ = c.change_property32(PropMode::REPLACE, win, ty, AtomEnum::ATOM, &[splash]); } - let _ = c.change_property8(PropMode::REPLACE, win, AtomEnum::WM_NAME, AtomEnum::STRING, b"Lemon Studio"); + let _ = c.change_property8(PropMode::REPLACE, win, AtomEnum::WM_NAME, AtomEnum::STRING, crate::version::NAME.as_bytes()); let _ = c.change_property8(PropMode::REPLACE, win, AtomEnum::WM_CLASS, AtomEnum::STRING, b"lemonstudio\0LemonStudio\0"); // No input region: clicks go through to what is behind. let _ = c.shape_rectangles(shape::SO::SET, shape::SK::INPUT, ClipOrdering::UNSORTED, win, 0, 0, &[]); @@ -1150,6 +1206,34 @@ mod tests { use super::*; use crate::prefs::{check, find_check, set_check, SHOW_SPLASH, SILENT_STARTUP}; + /// The colour at a point of the logo (its own units), the whole logo showing. + fn logo_pixel(p: &mut Painter, at: (f32, f32)) -> (u8, u8, u8) { + let look = Look { alpha: 1.0, scale: 1.0, progress: 1.0, light: 0.0, angle: 0.0, caption: String::new() }; + let k = DISC.2 / (2.0 * R); + let (x, y) = ((DISC.0 + (at.0 - C.0) * k) as u32, (DISC.1 + (at.1 - C.1) * k) as u32); + let frame = p.paint(&look); + let c = frame.pixel(x, y).unwrap().demultiply(); + (c.red(), c.green(), c.blue()) + } + + /// A nightly's splash is the Nightly logo: a night sky and a moon on the record half, the same lemon. + #[test] + fn the_nightly_splash_is_the_nightly_logo() { + let mut lemon = Painter::with_face(1.0, &FACE).unwrap(); + let mut night = Painter::with_face(1.0, &NIGHTLY_FACE).unwrap(); + // The record, away from the stars: black to grey on one, indigo on the other. + let at = (700.0, 300.0); + let (r, g, b) = logo_pixel(&mut lemon, at); + assert!(r == g && g == b, "{r} {g} {b}"); + let (r, g, b) = logo_pixel(&mut night, at); + assert!(b > r + 20 && b > g + 20, "{r} {g} {b}"); + // The moon, on the label. + assert_eq!(logo_pixel(&mut night, (560.0, 470.0)), (0xf6, 0xf2, 0xdf)); + // A slice of the lemon: the same in both. + let at = (450.0, 700.0); + assert_eq!(logo_pixel(&mut night, at), logo_pixel(&mut lemon, at)); + } + /// "Show startup splash screen" (spec: ui/settings-choices.md "Miscellaneous: naming, detached plugins and /// startup"): on as found, a program setting; with it off the start opens no splash. #[test] diff --git a/crates/os-app/src/update.rs b/crates/os-app/src/update.rs new file mode 100644 index 000000000..ac8e86255 --- /dev/null +++ b/crates/os-app/src/update.rs @@ -0,0 +1,560 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! Updates, for the builds the release workflow makes: a nightly build follows the nightlies, a stable one the stable +//! releases (`crate::version`). A build from source never updates itself. +//! +//! The work is done by `lemonstudio-updater` (`tools/updater`), which the release build carries inside itself +//! (`build.rs`, from `LEMON_UPDATER`). It is written to `/Updates` and started from there, so it can +//! replace the program it came from: +//! +//! 1. At start (General ▸ Updates ▸ Automatically check for updates), or from HELP ▸ Check for updates, it checks the +//! channel; with Automatically download program updates on it downloads a newer build right away. +//! 2. Lemon Studio offers it in a message box: Install and restart, Skip this version, Later. +//! 3. Install asks about unsaved changes as closing does, and closes; on the way out (`Updates::on_exit`) the updater +//! is started to wait for this process to end, put the new build in place of this one (the `.exe`, the AppImage +//! or the `.app`) and start it. +//! 4. The next start reports how it went (`Updates::startup`). +//! +//! Nothing here blocks: the updater runs on a thread of its own, and its answers are picked up by `App::update_tick`. +//! `OPENSTUDIO_NO_UPDATE_CHECK=1` turns the check at start off; `OPENSTUDIO_UPDATE_BASE` checks another releases URL. + +use std::io::BufRead; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::mpsc; + +use crate::version::{CHANNEL, VERSION}; + +/// The updater built into this program, from `LEMON_UPDATER` at build time (release builds only). +#[cfg(lemon_updater)] +const UPDATER: Option<&[u8]> = Some(include_bytes!(env!("LEMON_UPDATER_FILE"))); +#[cfg(not(lemon_updater))] +const UPDATER: Option<&[u8]> = None; + +const UPDATER_NAME: &str = if cfg!(windows) { "lemonstudio-updater.exe" } else { "lemonstudio-updater" }; + +/// Set to `1`: no check at start (HELP ▸ Check for updates still checks). +const NO_CHECK: &str = "OPENSTUDIO_NO_UPDATE_CHECK"; + +/// Another releases URL to check (testing an update against a server of one's own; the manifest must still carry the +/// release key's signature). +const BASE: &str = "OPENSTUDIO_UPDATE_BASE"; + +/// The version the user chose to skip (Skip this version), in the program's settings file. +const SKIPPED_KEY: &str = "update_skipped_version"; + +/// Whether this build updates itself: a release build carrying its updater. +pub fn enabled() -> bool { + CHANNEL.is_some() && UPDATER.is_some() +} + +/// `/Updates/`: the updater, the download, the outcome of the last update. A folder per channel: +/// a nightly and an alpha on one computer share the user folder, and must not take each other's download or report +/// each other's update. +fn folder() -> Option { + Some(os_paths::user_file("Updates")?.join(CHANNEL?)) +} + +/// What the update replaces: the program file on Windows, the AppImage on Linux, the `.app` on macOS. An error says +/// why this copy cannot be updated (in words for the user). +pub fn target() -> Result { + let exe = std::env::current_exe().map_err(|e| format!("Lemon Studio could not find its own program file: {e}"))?; + #[cfg(target_os = "linux")] + let target = { + let _ = exe; + match std::env::var_os("APPIMAGE").map(PathBuf::from).filter(|p| p.is_absolute()) { + Some(p) => p, + None => return Err("This copy of Lemon Studio is not running from its AppImage, so it can't update itself.".into()), + } + }; + #[cfg(target_os = "macos")] + let target = { + let Some(app) = exe.ancestors().find(|p| p.extension().is_some_and(|x| x == "app")).map(std::path::Path::to_path_buf) else { + return Err(format!("This copy of Lemon Studio is not inside its {}.app, so it can't update itself.", crate::version::NAME)); + }; + // An app opened from where it was downloaded (still quarantined) runs from a read-only copy macOS made. + if app.to_string_lossy().contains("/AppTranslocation/") { + return Err(format!("Move {}.app to your Applications folder and open it from there: macOS runs it from a read-only copy where it is now, so it can't update itself.", crate::version::NAME)); + } + app + }; + #[cfg(not(any(target_os = "linux", target_os = "macos")))] + let target = exe; + let dir = target.parent().ok_or("Lemon Studio's program file has no folder.")?; + let probe = dir.join(format!(".lemonstudio-update-check-{}", std::process::id())); + match std::fs::write(&probe, b"") { + Ok(()) => { + let _ = std::fs::remove_file(&probe); + Ok(target) + } + Err(_) => Err(format!("Lemon Studio can't replace itself in {}: that folder can't be written to. Download the new version from the release page instead.", dir.display())), + } +} + +/// The updater, written out of this program into the updates folder (again whenever it differs). +fn updater() -> Result { + let bytes = UPDATER.ok_or("This build has no updater.")?; + let dir = folder().ok_or("Lemon Studio has no user folder to update from.")?; + std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let path = dir.join(UPDATER_NAME); + if std::fs::read(&path).is_ok_and(|b| b == bytes) { + return Ok(path); + } + let tmp = dir.join(format!("{UPDATER_NAME}.{}.tmp", std::process::id())); + std::fs::write(&tmp, bytes).map_err(|e| format!("{}: {e}", tmp.display()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&tmp, std::fs::Permissions::from_mode(0o755)); + } + if let Err(e) = std::fs::rename(&tmp, &path) { + let _ = std::fs::remove_file(&tmp); + // Windows: an updater still running (from a moment ago) holds its file; that one is as good. + if !path.exists() { + return Err(format!("{}: {e}", path.display())); + } + } + Ok(path) +} + +/// What a check found (`Event::Found`). +#[derive(Debug, Clone, PartialEq)] +pub struct Found { + pub version: String, + /// What changed, a line each. + pub notes: Vec, + /// The release's page. + pub page: String, + /// The download, ready to install (None: not downloaded yet). + pub staged: Option, +} + +/// The updater's answers, as `App::update_tick` gets them. +#[derive(Debug, Clone, PartialEq)] +pub enum Event { + /// Downloading: bytes so far, of. + Progress(u64, u64), + Found(Found), + /// This is the newest build. + Current, + /// The channel has no release yet. + NoRelease, + /// The channel has no build for this platform. + Unsupported, + Failed(String), +} + +/// An answer line of the updater (`tools/updater`'s `main`); None for a line it does not know. +pub fn parse(line: &str) -> Option { + let v: serde_json::Value = serde_json::from_str(line).ok()?; + let text = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string(); + Some(match v.get("status")?.as_str()? { + "progress" => Event::Progress(v.get("done")?.as_u64()?, v.get("total")?.as_u64()?), + "available" | "staged" => Event::Found(Found { + version: text("version"), + notes: v.get("notes").and_then(|n| n.as_array()).map(|a| a.iter().filter_map(|l| l.as_str().map(String::from)).collect()).unwrap_or_default(), + page: text("page"), + staged: v.get("path").and_then(|p| p.as_str()).map(PathBuf::from), + }), + "current" => Event::Current, + "none" => Event::NoRelease, + "unsupported" => Event::Unsupported, + "error" => Event::Failed(text("message")), + _ => return None, + }) +} + +/// The update waiting to be put in place when Lemon Studio closes. +#[derive(Debug, Clone, PartialEq)] +pub struct Install { + pub staged: PathBuf, + pub version: String, + /// Start Lemon Studio again afterwards (Install and restart; not when the closing was put off and happens later). + pub relaunch: bool, +} + +#[derive(Default)] +pub struct Updates { + /// The updater's answers while it runs. + rx: Option>, + /// HELP ▸ Check for updates asked: the outcome is told even when there is nothing new, and a skipped version is + /// offered again. + pub asked: bool, + /// Install was chosen before the download was there: it installs once downloaded. + pub install_when_staged: bool, + /// The update found, while its message box waits for a moment without a dialog (and is shown). + pub offer: Option, + pub offer_due: bool, + pub install: Option, +} + +impl Updates { + /// Whether the updater is running. + pub fn busy(&self) -> bool { + self.rx.is_some() + } + + /// Starts the updater on a thread of its own: a check, and the download of a newer build when `download`. + pub fn start(&mut self, download: bool) -> Result<(), String> { + let (Some(channel), true) = (CHANNEL, enabled()) else { return Err("This build doesn't update itself.".into()) }; + let exe = updater()?; + let mut cmd = Command::new(exe); + cmd.args([if download { "download" } else { "check" }, "--channel", channel, "--current", VERSION]); + if download { + cmd.arg("--to").arg(folder().ok_or("no user folder")?.join("download")); + } + if let Some(base) = std::env::var_os(BASE).filter(|b| !b.is_empty()) { + cmd.arg("--base").arg(base); + } + cmd.stdin(Stdio::null()).stdout(Stdio::piped()).stderr(Stdio::null()); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + let mut child = cmd.spawn().map_err(|e| format!("The updater could not start: {e}"))?; + let out = child.stdout.take().expect("piped"); + let (tx, rx) = mpsc::channel(); + self.rx = Some(rx); + std::thread::Builder::new() + .name("update".into()) + .spawn(move || { + let mut answered = false; + for line in std::io::BufReader::new(out).lines().map_while(Result::ok) { + if let Some(e) = parse(&line) { + answered |= !matches!(e, Event::Progress(..)); + if tx.send(e).is_err() { + break; + } + crate::wake_ui(); + } + } + let status = child.wait(); + if !answered { + let why = status.map_or_else(|e| e.to_string(), |s| s.to_string()); + let _ = tx.send(Event::Failed(format!("the updater ended without an answer ({why})"))); + crate::wake_ui(); + } + }) + .map_err(|e| e.to_string())?; + Ok(()) + } + + /// The updater's answers since the last call. Its last one ends the run. + pub fn poll(&mut self) -> Vec { + let Some(rx) = &self.rx else { return Vec::new() }; + let mut out = Vec::new(); + loop { + match rx.try_recv() { + Ok(e) => out.push(e), + Err(mpsc::TryRecvError::Empty) => break, + Err(mpsc::TryRecvError::Disconnected) => { + self.rx = None; + break; + } + } + } + if out.iter().any(|e| !matches!(e, Event::Progress(..))) { + self.rx = None; + } + out + } + + /// At start: the outcome of the update the last run started (to tell the user: true when it worked, and what to + /// say), the program the update replaced removed (Windows), and the check at start begun when it is on. + pub fn startup(&mut self) -> Option<(bool, String)> { + #[cfg(windows)] + if let Ok(exe) = std::env::current_exe() { + let mut old = exe.into_os_string(); + old.push(".old"); + let _ = std::fs::remove_file(old); + } + let outcome = folder().map(|d| d.join("result.json")).and_then(|f| { + let text = std::fs::read_to_string(&f).ok(); + let _ = std::fs::remove_file(&f); + text + }); + let told = outcome.and_then(|t| serde_json::from_str::(&t).ok()).map(|v| { + let version = v.get("version").and_then(|x| x.as_str()).unwrap_or("").to_string(); + if v.get("ok").and_then(|x| x.as_bool()) == Some(true) { + (true, format!("Lemon Studio was updated to {version}.")) + } else { + let why = v.get("message").and_then(|x| x.as_str()).unwrap_or("unknown error"); + (false, format!("The update to Lemon Studio {version} could not be installed: {why}")) + } + }); + let off = std::env::var_os(NO_CHECK).is_some_and(|v| v == "1"); + if enabled() && !off && crate::prefs::check(&crate::prefs::AUTO_CHECK_UPDATES) && target().is_ok() { + if let Err(e) = self.start(crate::prefs::check(&crate::prefs::AUTO_DOWNLOAD_UPDATES)) { + eprintln!("Lemon Studio: update check: {e}"); + } + } + told + } + + /// The version the user skipped. + pub fn skipped() -> Option { + crate::settings_file::get(SKIPPED_KEY).filter(|v| !v.is_empty()) + } + + pub fn skip(version: &str) { + crate::settings_file::write(SKIPPED_KEY, version); + } + + /// Lemon Studio is closing: an update waiting to be installed hands over to the updater, which waits for this + /// process to end. + pub fn on_exit(&mut self) { + let Some(install) = self.install.take() else { return }; + if let Err(e) = apply(&install) { + eprintln!("Lemon Studio: the update could not start: {e}"); + if let Some(f) = folder() { + let record = serde_json::json!({ "ok": false, "version": install.version, "message": e }); + let _ = std::fs::write(f.join("result.json"), record.to_string()); + } + } + } +} + +/// Starts the updater's `apply` for `install`, on its own (it outlives this process). +fn apply(install: &Install) -> Result<(), String> { + let exe = updater()?; + let target = target()?; + let result = folder().ok_or("no user folder")?.join("result.json"); + let mut cmd = Command::new(exe); + cmd.arg("apply").arg("--pid").arg(std::process::id().to_string()); + cmd.arg("--staged").arg(&install.staged).arg("--target").arg(&target); + cmd.arg("--version").arg(&install.version).arg("--result").arg(&result); + if install.relaunch { + cmd.arg("--relaunch"); + } + cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + if let Some(dir) = result.parent() { + cmd.current_dir(dir); + } + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + const CREATE_BREAKAWAY_FROM_JOB: u32 = 0x0100_0000; + // Out of a job that would end it with this process, where the job allows that. + cmd.creation_flags(CREATE_NO_WINDOW | CREATE_BREAKAWAY_FROM_JOB); + if cmd.spawn().is_ok() { + return Ok(()); + } + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd.spawn().map(drop).map_err(|e| e.to_string()) +} + +/// The message box's text: the version, what changed (at most `MAX_NOTES` lines) and what Install does. +pub fn offer_text(f: &Found) -> Vec<(String, bool)> { + const MAX_NOTES: usize = 8; + let mut t = vec![ + ("Lemon Studio ".to_string(), false), + (f.version.clone(), true), + (format!(" is available. You have {}.", VERSION), false), + ]; + if !f.notes.is_empty() { + let mut s = String::from("\n\nWhat's new:"); + for n in f.notes.iter().take(MAX_NOTES) { + s.push_str(&format!("\n\u{2022} {n}")); + } + if f.notes.len() > MAX_NOTES { + s.push_str(&format!("\n\u{2026} and {} more", f.notes.len() - MAX_NOTES)); + } + t.push((s, false)); + } + t.push(("\n\nInstalling closes Lemon Studio (asking about unsaved changes first) and opens the new version.".into(), false)); + t +} + +impl crate::app::App { + /// At start, once the window's state is restored: how the last update went, and the check at start. + pub(crate) fn update_startup(&mut self) { + use crate::dialog::{spans, Answer, Kind, Msg, Then}; + match self.updates.startup() { + Some((true, m)) => self.state.message = Some(m), + Some((false, m)) => self.show_msg(Msg::new(Kind::Error, "Update", spans(&[&m]), &[Answer::Ok], Then::Nothing)), + None => {} + } + } + + /// HELP ▸ Check for updates. + pub(crate) fn check_updates_now(&mut self) { + use crate::dialog::{spans, Answer, Kind, Msg, Then}; + let info = |app: &mut Self, text: &str| { + app.show_msg(Msg::new(Kind::Info, "Check for updates", spans(&[text]), &[Answer::Ok], Then::Nothing)); + }; + if !enabled() { + return info( + self, + "This copy of Lemon Studio was built from its source code, so it doesn't update itself: update it with git pull, then cargo run -p os-app --release (see the README).", + ); + } + if let Err(e) = target() { + return info(self, &e); + } + if self.updates.busy() { + self.updates.asked = true; + self.state.message = Some("Already checking for updates".into()); + return; + } + match self.updates.start(crate::prefs::check(&crate::prefs::AUTO_DOWNLOAD_UPDATES)) { + Ok(()) => { + self.updates.asked = true; + self.state.message = Some("Checking for updates".into()); + } + Err(e) => info(self, &e), + } + } + + /// The updater's answers, and the update offered once no dialog is in the way (and nothing is being recorded). + pub(crate) fn update_tick(&mut self) { + use crate::dialog::{spans, Answer, Kind, Msg, Then}; + for e in self.updates.poll() { + let asked = self.updates.asked || self.updates.install_when_staged; + match e { + Event::Progress(done, total) => { + if asked { + let pct = if total > 0 { done * 100 / total } else { 0 }; + self.state.message = Some(format!("Downloading the update: {pct}%")); + } + } + Event::Found(f) => { + let wanted = std::mem::take(&mut self.updates.install_when_staged); + if wanted && f.staged.is_some() { + self.updates.asked = false; + self.install_update(f); + continue; + } + if !self.updates.asked && Updates::skipped().as_deref() == Some(f.version.as_str()) { + continue; + } + self.updates.asked = false; + self.updates.offer = Some(f); + self.updates.offer_due = true; + } + other => { + self.updates.install_when_staged = false; + self.updates.asked = false; + let channel = crate::version::releases(); + let text = match other { + Event::Current => format!("You have the newest version of Lemon Studio, {}.", crate::version::text()), + Event::NoRelease => format!("There is no {channel} release of Lemon Studio yet."), + Event::Unsupported => format!("The newest {channel} release of Lemon Studio has no build for this system yet."), + Event::Failed(m) => { + eprintln!("Lemon Studio: update check: {m}"); + format!("Lemon Studio could not check for updates: {m}") + } + Event::Progress(..) | Event::Found(_) => continue, + }; + // The check at start keeps quiet about these. + if asked { + self.state.message = None; + self.show_msg(Msg::new(Kind::Info, "Check for updates", spans(&[&text]), &[Answer::Ok], Then::Nothing)); + } + } + } + } + let recording = self.audio_rec.is_some() || self.track_rec.is_some(); + if self.updates.offer_due && !self.modal_open() && !recording { + self.updates.offer_due = false; + if let Some(f) = self.updates.offer.clone() { + let install = if f.staged.is_some() { "Install and restart" } else { "Download and install" }; + let m = Msg::new(Kind::Info, "Update available", offer_text(&f), &[Answer::Yes, Answer::No, Answer::Cancel], Then::Update(f)) + .labelled(&[install, "Skip this version", "Later"]); + self.show_msg(m); + } + } + } + + /// The update's message box was answered: Install (Yes), Skip this version (No) or Later (Cancel). + pub(crate) fn update_answer(&mut self, f: Found, answer: crate::dialog::Answer) { + use crate::dialog::{spans, Answer, Kind, Msg, Then}; + match answer { + Answer::Yes if f.staged.is_some() => self.install_update(f), + Answer::Yes => { + self.updates.install_when_staged = true; + match self.updates.start(true) { + Ok(()) => self.state.message = Some("Downloading the update".into()), + Err(e) => { + self.updates.install_when_staged = false; + self.show_msg(Msg::new(Kind::Error, "Update", spans(&[&e]), &[Answer::Ok], Then::Nothing)); + } + } + } + Answer::No => Updates::skip(&f.version), + _ => {} + } + } + + /// Installs the downloaded update `f`: Lemon Studio closes (asking about unsaved changes first) and the updater + /// takes over (`Updates::on_exit`). + fn install_update(&mut self, f: Found) { + let Some(staged) = f.staged else { return }; + self.updates.offer = None; + self.updates.install = Some(Install { staged, version: f.version, relaunch: true }); + self.ask_discard("updating", crate::dialog::Next::Close); + } + + /// Closing for the update was called off (Cancel to the unsaved changes): it installs when Lemon Studio closes, + /// without starting it again. + pub(crate) fn update_put_off(&mut self) { + if let Some(i) = self.updates.install.as_mut().filter(|i| i.relaunch) { + i.relaunch = false; + self.state.message = Some(format!("Lemon Studio {} installs when Lemon Studio closes", i.version)); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_updaters_answers_are_read() { + assert_eq!(parse(r#"{"status":"progress","done":5,"total":10}"#), Some(Event::Progress(5, 10))); + assert_eq!(parse(r#"{"status":"current","version":"2026.11.7.1"}"#), Some(Event::Current)); + assert_eq!(parse(r#"{"status":"none"}"#), Some(Event::NoRelease)); + assert_eq!(parse(r#"{"status":"unsupported","version":"1.0.0","platform":"linux-riscv64"}"#), Some(Event::Unsupported)); + assert_eq!(parse(r#"{"message":"offline","status":"error"}"#), Some(Event::Failed("offline".into()))); + let staged = r#"{"status":"staged","version":"2026.11.8.1","path":"/u/Updates/download/a.AppImage","notes":["One","Two"],"page":"https://x"}"#; + assert_eq!( + parse(staged), + Some(Event::Found(Found { + version: "2026.11.8.1".into(), + notes: vec!["One".into(), "Two".into()], + page: "https://x".into(), + staged: Some("/u/Updates/download/a.AppImage".into()), + })) + ); + let available = parse(r#"{"status":"available","version":"1.0.1","notes":[],"page":"","size":3}"#); + assert!(matches!(available, Some(Event::Found(Found { staged: None, .. })))); + assert_eq!(parse("not json"), None); + assert_eq!(parse(r#"{"status":"later"}"#), None); + } + + #[test] + fn a_long_list_of_changes_is_cut_short() { + let f = Found { version: "2026.11.8.1".into(), notes: (1..=12).map(|i| format!("Change {i}")).collect(), page: String::new(), staged: None }; + let text: String = offer_text(&f).into_iter().map(|(s, _)| s).collect(); + assert!(text.contains("\u{2022} Change 8") && !text.contains("Change 9"), "{text}"); + assert!(text.contains("and 4 more"), "{text}"); + } + + #[test] + fn a_build_from_source_does_not_update() { + if option_env!("LEMON_CHANNEL").is_none() { + assert!(!enabled()); + assert!(Updates::default().start(false).is_err()); + } + } +} diff --git a/crates/os-app/src/version.rs b/crates/os-app/src/version.rs new file mode 100644 index 000000000..66275e4da --- /dev/null +++ b/crates/os-app/src/version.rs @@ -0,0 +1,91 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! Which Lemon Studio this is. The release workflow (`.github/workflows/release.yml`) builds with `LEMON_CHANNEL` +//! (`nightly` or `stable`) and `LEMON_VERSION` set: a nightly's version is dated, `YEAR.MONTH.DAY.N`; a tagged +//! release's is semantic, `MAJOR.MINOR.PATCH` (the workspace's `version`), and while its major version is 0 it is an +//! alpha. A build from source has neither: it is the workspace's version and belongs to no channel, so it never updates +//! itself (`crate::update`). + +/// This build's version. +pub const VERSION: &str = match option_env!("LEMON_VERSION") { + Some(v) => v, + None => env!("CARGO_PKG_VERSION"), +}; + +/// The release channel this build came from: `nightly` or `stable`; None for a build from source. +pub const CHANNEL: Option<&str> = option_env!("LEMON_CHANNEL"); + +/// Whether this is a nightly build, which has its own name, icon and splash. +pub const NIGHTLY: bool = cfg!(lemon_nightly); + +/// The program's name, where the system shows it (the window's title, the file association). +pub const NAME: &str = if NIGHTLY { "Lemon Studio Nightly" } else { "Lemon Studio" }; + +/// The version as Lemon Studio shows it: "2026.11.7.1 nightly", "1.0.0", or "0.0.1" for a build from source. +pub fn text() -> &'static str { + static TEXT: std::sync::OnceLock = std::sync::OnceLock::new(); + TEXT.get_or_init(|| match CHANNEL { + Some("nightly") => format!("{VERSION} nightly"), + _ => VERSION.to_string(), + }) +} + +/// The releases a build on `channel` follows, in words: `nightly`, `alpha` (a tagged release's version 0.x) or `stable`. +fn releases_of(channel: Option<&str>, version: &str) -> &'static str { + match channel { + Some("nightly") => "nightly", + _ if version.starts_with("0.") => "alpha", + _ => "stable", + } +} + +/// The releases this build follows, in words. +pub fn releases() -> &'static str { + releases_of(CHANNEL, VERSION) +} + +/// How far along a build says it is: a tagged release says what it is, an alpha or nothing at all. What is not tagged +/// (a nightly, a build from source) comes before the first alpha, until that is out and this changes. +fn stage_of(channel: Option<&str>, version: &str) -> Option<&'static str> { + match channel { + Some("stable") => (releases_of(channel, version) == "alpha").then_some("alpha"), + _ => Some("pre-alpha"), + } +} + +/// `text` with the stage: "2026.11.7.1 nightly (pre-alpha)", "0.1.0 (alpha)", "1.0.0". +pub fn staged() -> &'static str { + static TEXT: std::sync::OnceLock = std::sync::OnceLock::new(); + TEXT.get_or_init(|| match stage_of(CHANNEL, VERSION) { + Some(stage) => format!("{} ({stage})", text()), + None => text().to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_build_from_source_is_the_workspace_version() { + if CHANNEL.is_none() { + assert_eq!(text(), env!("CARGO_PKG_VERSION")); + } + } + + /// Alpha is in the version's number, not in the tag: v0.1.0 is an alpha, v1.0.0 is not. + #[test] + fn a_tagged_release_says_if_it_is_an_alpha() { + assert_eq!(stage_of(Some("stable"), "0.1.0"), Some("alpha")); + assert_eq!(stage_of(Some("stable"), "0.12.3"), Some("alpha")); + assert_eq!(stage_of(Some("stable"), "1.0.0"), None); + assert_eq!(stage_of(Some("stable"), "10.0.0"), None); + assert_eq!(stage_of(Some("nightly"), "2026.11.8.1"), Some("pre-alpha")); + assert_eq!(stage_of(None, "0.0.1"), Some("pre-alpha")); + assert_eq!(releases_of(Some("stable"), "0.1.0"), "alpha"); + assert_eq!(releases_of(Some("stable"), "1.0.0"), "stable"); + assert_eq!(releases_of(Some("nightly"), "2026.11.8.1"), "nightly"); + } +} diff --git a/crates/os-app/src/views/menus.rs b/crates/os-app/src/views/menus.rs index 7a1d956b9..e46e89c99 100644 --- a/crates/os-app/src/views/menus.rs +++ b/crates/os-app/src/views/menus.rs @@ -70,6 +70,8 @@ pub enum MenuCmd { SaveAudioClip, /// HELP > Diagnostics (`crate::diagnostics`). Diagnostics, + /// HELP > Check for updates (`crate::update`). + CheckUpdates, /// HELP's entries that open a page in the web browser (`crate::help_links`). Web(&'static str), /// The menu bar's ways into the browser. @@ -223,7 +225,7 @@ pub const OPTIONS_MENU: usize = 5; pub const OPT_INFO: usize = 9; /// HELP > About (the toolbar logo). pub const HELP_MENU: usize = 7; -pub const HELP_ABOUT: usize = 6; +pub const HELP_ABOUT: usize = 7; /// Index of the ADD and TOOLS menus. pub const ADD_MENU: usize = 2; pub const TOOLS_MENU: usize = 6; @@ -434,6 +436,7 @@ const HELP: &[Item] = &[ head("Support"), it("Diagnostics", "").on().cmd(MenuCmd::Diagnostics), it("What's new", "").on().cmd(MenuCmd::Web(crate::help_links::WHATS_NEW_URL)), + it("Check for updates", "").on().cmd(MenuCmd::CheckUpdates), SEP, it("About...", "").on(), ]; diff --git a/crates/os-app/src/views/mod.rs b/crates/os-app/src/views/mod.rs index 33541f496..b0842fd57 100644 --- a/crates/os-app/src/views/mod.rs +++ b/crates/os-app/src/views/mod.rs @@ -184,10 +184,10 @@ pub fn paint(ui: &mut Ui, s: &State, f: &FrameInfo) -> usize { if docked.is_none() && !crate::wallpaper::paint(ui, work) { let wm = ui.t.watermark; icons::logo(ui.c, work.cx(), work.cy() + 40.0, 170.0, wm); - let version = concat!("Lemon Studio v", env!("CARGO_PKG_VERSION"), " - pre-alpha"); + let version = format!("Lemon Studio v{}", crate::version::staged()); let vc = ui.t.version_text; - let vw = ui.c.text_width(crate::views::fnt(16.0), version); - ui.c.text(crate::views::fnt(16.0), work.cx() - vw / 2.0, h - 22.0, vc, version); + let vw = ui.c.text_width(crate::views::fnt(16.0), &version); + ui.c.text(crate::views::fnt(16.0), work.cx() - vw / 2.0, h - 22.0, vc, &version); } // One stacking order for all child windows (spec: ui/channel-rack.md §5). A maximized window diff --git a/crates/os-app/src/views/settings.rs b/crates/os-app/src/views/settings.rs index 38502e8f6..54954bd73 100644 --- a/crates/os-app/src/views/settings.rs +++ b/crates/os-app/src/views/settings.rs @@ -295,8 +295,22 @@ pub const LIVE: &[&str] = &[ "Reset", ASSOCIATE_CHECK, SHOW_ASIO_PANEL, + CHECK_UPDATES, ]; +/// About: checks the build's channel for a newer build (`App::check_updates_now`, as HELP ▸ Check for updates). +pub const CHECK_UPDATES: &str = "Check for updates"; + +/// About: which build this is and how it is kept up to date (`crate::update`). +fn update_lines() -> Vec { + let how = match (crate::version::CHANNEL, crate::update::enabled()) { + (Some("nightly"), true) => "A nightly build of main. It updates itself from the nightly releases.".to_string(), + (Some(_), true) => format!("It updates itself from the {} releases.", crate::version::releases()), + _ => "Built from its source code: it doesn't update itself (git pull, then build it again).".to_string(), + }; + vec![txt(&how), Row::Buttons(vec![(CHECK_UPDATES, 130.0)])] +} + /// MIDI: lists the devices again (`App::midi_refresh`); Update MIDI scripts stays greyed with its reason. pub const REFRESH_MIDI: &str = "Refresh device list"; pub const UPDATE_SCRIPTS: &str = "Update MIDI scripts"; @@ -748,8 +762,8 @@ pub fn page(tab: usize, s: &State) -> Vec { ]), gf("System", false, vec![Check(ASSOCIATE_CHECK, s.settings.assoc)]), // FL's "Updates and privacy" without Send usage data and its More information button: Lemon Studio collects - // nothing. The update checks stay greyed until Lemon Studio can update itself (spec: ui/settings.md - // "General"). + // nothing. The checks are `prefs::AUTO_CHECK_UPDATES` and `AUTO_DOWNLOAD_UPDATES` (`crate::update`; a + // build from source does not update itself) (spec: ui/settings.md "General"). gf("Updates", false, vec![ Check("Automatically check for updates", true), Check("Automatically download program updates", true), @@ -921,7 +935,7 @@ pub fn page(tab: usize, s: &State) -> Vec { } DEBUG => { let mut lines = vec![ - format!("Lemon Studio {}", env!("CARGO_PKG_VERSION")), + format!("Lemon Studio {}", crate::version::text()), format!("Project: {}", s.path.as_ref().map_or("(new project)".into(), |p| p.display().to_string())), format!("Saved by: {}", s.project.saved_by), format!("PPQ {}, tempo {:.3}", s.project.ppq, s.tempo), @@ -943,11 +957,13 @@ pub fn page(tab: usize, s: &State) -> Vec { g("", [ vec![ Gap(20.0), - Big(format!("Lemon Studio {}", env!("CARGO_PKG_VERSION"))), + Big(format!("Lemon Studio {}", crate::version::staged())), txt("A digital audio workstation written in Rust."), txt("Copyright (C) 2026 ItsAlphaNeon and the Lemon Studio contributors."), Gap(10.0), ], + update_lines(), + vec![Gap(6.0)], if cfg!(windows) { vec![txt("Licence: GNU General Public License, version 3 (the ASIO SDK it includes is version 3 only).")] } else { @@ -968,7 +984,8 @@ pub fn page(tab: usize, s: &State) -> Vec { txt("It is free software: you can redistribute it and/or modify it under the terms"), txt("of the GNU General Public License, version 3 or (at your option) any later version."), txt("It comes with ABSOLUTELY NO WARRANTY. Licence: https://www.gnu.org/licenses/"), - txt("No registration, no online services, no usage data."), + txt("No registration and no usage data. Release builds ask GitHub for updates"), + txt("(Settings > General > Updates); a build from source never goes online."), ]), ], } diff --git a/docs/alpha-checklist.md b/docs/alpha-checklist.md index 031261469..467e10b21 100644 --- a/docs/alpha-checklist.md +++ b/docs/alpha-checklist.md @@ -236,14 +236,20 @@ Left out of the alpha on 2026-11-02; each stays open here until it is done. ## Shipping a build -What has to exist before anyone outside can run the alpha. Builds ship once the rest of this checklist is done -(decided 2026-10-29), so these come last: until then nothing is packaged or released, and contributions that add -installers or release packaging are declined. +What has to exist before anyone outside can run the alpha. Tagged releases ship once the rest of this checklist is +done (decided 2026-10-29), so these come last; a tag with major version 0, like `v0.1.0`, is an alpha. Nightly builds +of main are published before that (decided 2026-11-07), by the release workflow ([releasing.md](releasing.md)); +contributions that add other installers or packaging are declined. -- [ ] **Prebuilt binaries for Windows, macOS and Linux.** Today Lemon Studio is built from source only +- [ ] **Prebuilt binaries for Windows, macOS and Linux.** The release workflow builds them, a nightly from main every + night and a tagged release from a `vMAJOR.MINOR.PATCH` tag: a Windows `.exe`, a Linux AppImage and a macOS `.app` + (Apple silicon), each updating itself (`lemonstudio-updater`), and the plugins for other DAWs as a zip per platform. + Left: the steps under "Going live" in [releasing.md](releasing.md), its first run and trying the updates on each + platform among them - [x] **Complete [THIRD-PARTY-NOTICES.txt](../THIRD-PARTY-NOTICES.txt).** Every crate that goes into a build is listed with its licence text, generated by `cargo run -p notices`; `cargo test` fails when one is missing -- [ ] **macOS signing and notarization.** Builds and plugin bundles are signed ad hoc only +- [x] **macOS signing and notarization.** Not done: builds and plugin bundles are signed ad hoc only, as notarization + needs a paid Apple Developer account. The release notes say how to open them anyway (decided 2026-11-07) - [ ] **Plugin crashes contained on macOS and Linux.** Calls into plugins run under a signal guard there, with the same handling of a stopped plugin as on Windows (its crash tests pass in CI on both), and so do CLAP and VST3 editors' run loop callbacks on Linux. Left: macOS editors, which the system's run loop calls outside any host diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 000000000..fbd1de2a0 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,157 @@ +# Releasing Lemon Studio + +Lemon Studio is released by [.github/workflows/release.yml](../.github/workflows/release.yml) on two channels. Each +build updates itself on its own channel. A build from source has no channel and never updates itself. + +| Channel | Made by | Version | GitHub release | +|---|---|---|---| +| Nightly | Every night at 04:00 UTC from `main`, once turned on, if `main` moved since the last nightly; or by hand (Actions ▸ Release ▸ Run workflow, with *force* to build one anyway) | Dated: `YEAR.MONTH.DAY.N` (UTC, no zero padding, `N` counts that day's nightlies from 1), e.g. `2026.11.7.1` | Pre-release `nightly-2026.11.7.1`; the pre-release `nightly` holds `nightly.json`, pointing at the newest. The newest 14 are kept | +| Tagged | Pushing a tag `vMAJOR.MINOR.PATCH` on `main` | Semantic: the workspace's `version` in `Cargo.toml`, which the tag must match, e.g. `0.1.0` | Release `v0.1.0`, holding `stable.json`; GitHub's *latest* release | + +**A tagged release with major version 0 is an alpha.** The tag has no `alpha` in it: `v0.1.0`, `v0.1.1` and `v0.2.0` are +alphas, and `v1.0.0` is the first stable release. Lemon Studio, the diagnostics and the release's title say which it is +([version.rs](../crates/os-app/src/version.rs)), and installed alphas update on to the first stable release like any +other newer version. Alphas are not marked as pre-releases on GitHub, because the updater finds the newest one as +GitHub's *latest* release. In the workflow and the manifest the channel is called `stable`, alphas included. + +Each release has: + +- `LemonStudio.exe` (a nightly: `LemonStudioNightly.exe`) +- `LemonStudio.AppImage` (a nightly: `LemonStudioNightly.AppImage`), built on Ubuntu 22.04 by [tools/package/appimage.sh](../tools/package/appimage.sh) +- `LemonStudio--macos-aarch64.zip`, holding `Lemon Studio.app` (a nightly: `Lemon Studio Nightly.app`), signed + ad hoc and not notarized +- `LemonStudio-Plugins--.zip`: the VST3 and CLAP plugins for other DAWs, with a `README.txt` on where they go +- `nightly.json` or `stable.json`, and its minisign signature (`.minisig`) +- `LICENSE` and `THIRD-PARTY-NOTICES.txt` + +The `.exe` and the AppImage have no version in their names: an update replaces the file in place, keeping its name, so +shortcuts to it never break. The release's title and Lemon Studio itself say which version it is. + +The release notes come from [tools/package/release-notes.md](../tools/package/release-notes.md) (how to install and +open the builds, including the macOS steps past Gatekeeper), followed by GitHub's list of the pull requests since the +previous release on that channel. The first release on a channel has no such list. + +## What makes a nightly look like one + +With `LEMON_CHANNEL=nightly`, a build has the Nightly logo and its own name, Lemon Studio Nightly, so that it can sit +beside a tagged release on one computer: the two share the user folder (settings, projects), but keep their updates +apart. + +| Where | What changes | Made from | +|---|---|---| +| Windows | The `.exe`'s file name, icon and version resource, the window's title | `assets/lemonstudio-nightly.ico` | +| Linux | The AppImage's file name, desktop entry and icon, the window's app id and icon | `assets/lemonstudio-nightly.png` | +| macOS | The app's name, identifier and icon | `assets/lemonstudio-nightly.car` and `.icns` | +| Startup splash | The moon, its craters, the night sky and the stars | `crate::logo`'s `NIGHTLY_*`, [splash.rs](../crates/os-app/src/splash.rs) | + +All of these are in `crates/os-app/assets` and are committed, so a build needs no tools to make them. To change them, +edit the sources in `assets/logo`, then regenerate: + +- `icon-lemon-nightly.png` (edited in `icon-lemon-nightly.pdn`, Paint.NET) and `logo-colored-lemon-nightly.svg`: + `python tools/logo/gen.py`, which needs Pillow. +- `macOS-nightly.icon`, which has its own copy of the SVG: `tools/logo/macos-icon.sh nightly`, on macOS with Xcode 26. + +## How a build updates itself + +The workflow builds `lemonstudio-updater` ([tools/updater](../tools/updater)) first. It then builds Lemon Studio with +these environment variables set: + +- `LEMON_CHANNEL` and `LEMON_VERSION`: [crates/os-app/src/version.rs](../crates/os-app/src/version.rs) +- `LEMON_UPDATER`: the updater, which the program carries inside itself (`build.rs`) +- `LEMON_UPDATE_KEY`: the public key the updater checks manifests with + +At start, Lemon Studio writes the updater to `/Updates/` and runs it there. The updater reads the +channel's manifest: + +- nightly: `releases/download/nightly/nightly.json` +- tagged: `releases/latest/download/stable.json` + +It checks the manifest's signature and compares versions: dated with dated, semantic with semantic, and never accepts an +older version. If there is a newer version, it downloads this platform's file and checks its size and SHA-256. Lemon +Studio then offers the update. If the user installs it, Lemon Studio closes and the updater replaces the `.exe`, the +AppImage (`$APPIMAGE`) or the `.app`, then starts the new version. The next start reports how it went. The +module docs in [update.rs](../crates/os-app/src/update.rs) and the updater's `main.rs` have the details. + +A copy that can't replace itself says so and doesn't offer the update: + +- a Linux build not run from its AppImage +- a macOS app run from where it was downloaded (macOS runs it from a read-only copy) +- a program in a folder the user can't write to + +To try an update against a server of your own, set `OPENSTUDIO_UPDATE_BASE` to its releases URL. The manifest must +still be signed with the release key. `OPENSTUDIO_NO_UPDATE_CHECK=1` turns off the check at start. + +## Going live (once) + +Merging the workflow publishes nothing: its schedule stays off, and a run by hand refuses to start without the signing +key. In this order: + +### 1. The signing key + +Manifests are signed with [minisign](https://jedisct1.github.io/minisign/) (Windows: `winget install jedisct1.minisign`). +Make a key pair without a password; the GitHub secret store protects it: + +```bash +minisign -G -W -p lemonstudio-update.pub -s lemonstudio-update.key +gh secret set MINISIGN_SECRET_KEY < lemonstudio-update.key +gh variable set MINISIGN_PUBLIC_KEY --body "$(tail -n1 lemonstudio-update.pub)" +``` + +Keep `lemonstudio-update.key` somewhere safe and offline, and don't commit it. Every installed copy trusts only this +key. If the key is lost or replaced, the copies already out there can't verify new manifests, and their users have to +download a new build by hand. The workflow refuses to run without both settings, and it checks each signature against +the public key before publishing. + +### 2. Rehearse + +A dry run builds, packages and signs everything, then keeps the files as the run's artifacts instead of publishing them. +Nothing is created on the releases page. Rehearse a nightly and a tag (the tag must match `Cargo.toml`, and need not +exist): + +```bash +gh workflow run release.yml -f dry_run=true +``` + +```bash +gh workflow run release.yml -f dry_run=true -f rehearse_tag=v0.0.1 +``` + +Then download the artifacts and open each platform's build. A rehearsed build can't update itself, as there is no +release for it to find yet. + +### 3. Publish the first nightly and turn the schedule on + +```bash +gh workflow run release.yml +``` + +```bash +gh variable set NIGHTLIES --body on +``` + +The website's download buttons come alive at its next rebuild, 05:30 UTC each night. + +### 4. Try an update on each platform + +An update needs two builds. With the first nightly installed on each system, publish a second by hand with *force*, and +check that each one offers it, installs it and starts again. + +## Making a tagged release + +1. Set `version` under `[workspace.package]` in `Cargo.toml`, e.g. `0.1.0`, run `cargo check` so that `Cargo.lock` + follows, and merge it. +2. Tag the merge commit and push the tag: + + ```bash + git switch main && git pull && git tag v0.1.0 && git push origin v0.1.0 + ``` + +The workflow refuses a tag that isn't `vMAJOR.MINOR.PATCH`, doesn't match `Cargo.toml` or isn't on `main`. + +Once the first alpha is out, nightlies and builds from source stop being "pre-alpha": change `stage_of` in +[version.rs](../crates/os-app/src/version.rs). + +## Testing the workflow's scripts + +`tools/package/test.sh` tests the release plan and the manifest without building anything, and CI runs it. CI also +checks that a nightly's and a tagged release's settings compile on every platform. diff --git a/tools/bundle-app/src/main.rs b/tools/bundle-app/src/main.rs index 331fe0b53..b9e5a9adf 100644 --- a/tools/bundle-app/src/main.rs +++ b/tools/bundle-app/src/main.rs @@ -11,14 +11,33 @@ //! bundle is signed ad hoc, as Apple silicon requires of any program it runs. `--install` copies it to //! `~/Applications`. //! +//! The release workflow builds with `LEMON_VERSION` set (a nightly's `YEAR.MONTH.DAY.N`, or a release's semantic +//! version), which the bundle's version follows; otherwise it is the workspace's. With `LEMON_CHANNEL` set to `nightly` +//! it is `Lemon Studio Nightly.app`, an app of its own beside Lemon Studio, with the icon made from +//! `assets/logo/macOS-nightly.icon` (`tools/logo/macos-icon.sh nightly`). +//! //! The macOS counterpart of Linux's desktop entry (`crates/os-app/assets/lemonstudio.desktop`). use std::path::PathBuf; use std::process::Command; -const NAME: &str = "Lemon Studio"; const EXE: &str = "lemonstudio"; -const IDENTIFIER: &str = "io.github.itsalphaneon.lemonstudio"; + +/// What the bundle is called, and which icon it has. +struct Identity { + name: &'static str, + identifier: &'static str, + /// The stem of the icon files in `assets`. + icon: &'static str, +} + +fn identity(channel: Option<&str>) -> Identity { + if channel == Some("nightly") { + Identity { name: "Lemon Studio Nightly", identifier: "io.github.itsalphaneon.lemonstudio.nightly", icon: "lemonstudio-nightly" } + } else { + Identity { name: "Lemon Studio", identifier: "io.github.itsalphaneon.lemonstudio", icon: "lemonstudio" } + } +} fn main() { if !cfg!(target_os = "macos") { @@ -26,6 +45,7 @@ fn main() { std::process::exit(1); } let install = std::env::args().any(|a| a == "--install"); + let id = identity(std::env::var("LEMON_CHANNEL").ok().as_deref()); let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().expect("repository root"); let target = std::env::var_os("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|| root.join("target")); let target = if target.is_relative() { root.join(target) } else { target }; @@ -38,7 +58,7 @@ fn main() { .success(); assert!(ok, "build of os-app failed"); - let app = target.join("bundled").join(format!("{NAME}.app")); + let app = target.join("bundled").join(format!("{}.app", id.name)); if app.exists() { std::fs::remove_dir_all(&app).expect("remove the old bundle"); } @@ -47,17 +67,17 @@ fn main() { std::fs::create_dir_all(&macos).expect("bundle folders"); std::fs::create_dir_all(&resources).expect("bundle folders"); std::fs::copy(target.join("release").join(EXE), macos.join(EXE)).expect("copy the program"); - std::fs::write(contents.join("Info.plist"), info_plist()).expect("Info.plist"); + std::fs::write(contents.join("Info.plist"), info_plist(&id)).expect("Info.plist"); std::fs::write(contents.join("PkgInfo"), "APPL????").expect("PkgInfo"); let assets = root.join("crates/os-app/assets"); - std::fs::copy(assets.join(format!("{EXE}.car")), resources.join("Assets.car")).expect("copy the icon"); - std::fs::copy(assets.join(format!("{EXE}.icns")), resources.join(format!("{EXE}.icns"))).expect("copy the icon"); + std::fs::copy(assets.join(format!("{}.car", id.icon)), resources.join("Assets.car")).expect("copy the icon"); + std::fs::copy(assets.join(format!("{}.icns", id.icon)), resources.join(format!("{EXE}.icns"))).expect("copy the icon"); run(Command::new("codesign").args(["--force", "--sign", "-"]).arg(&app), "codesign"); println!("{}", app.display()); if install { let dir = PathBuf::from(std::env::var_os("HOME").expect("HOME")).join("Applications"); - let to = dir.join(format!("{NAME}.app")); + let to = dir.join(format!("{}.app", id.name)); if to.exists() { std::fs::remove_dir_all(&to).expect("remove the installed bundle"); } @@ -72,21 +92,30 @@ fn run(cmd: &mut Command, what: &str) { assert!(cmd.status().unwrap_or_else(|e| panic!("{what}: {e}")).success(), "{what} failed"); } -fn info_plist() -> String { - let version = env!("CARGO_PKG_VERSION"); +/// The bundle's (short version, build version): `CFBundleShortVersionString` takes three numbers at most, so a +/// nightly's `2026.11.7.1` is `2026.11.7` there and whole as `CFBundleVersion`. +fn versions() -> (String, String) { + let full = std::env::var("LEMON_VERSION").ok().filter(|v| !v.is_empty()).unwrap_or_else(|| env!("CARGO_PKG_VERSION").into()); + let short = full.split(['-', '+']).next().unwrap_or("").split('.').take(3).collect::>().join("."); + (short, full) +} + +fn info_plist(id: &Identity) -> String { + let (short, version) = versions(); + let Identity { name, identifier, .. } = id; format!( r#" CFBundleName - {NAME} + {name} CFBundleDisplayName - {NAME} + {name} CFBundleExecutable {EXE} CFBundleIdentifier - {IDENTIFIER} + {identifier} CFBundleIconFile {EXE} CFBundleIconName @@ -94,7 +123,7 @@ fn info_plist() -> String { CFBundlePackageType APPL CFBundleShortVersionString - {version} + {short} CFBundleVersion {version} CFBundleInfoDictionaryVersion @@ -127,3 +156,24 @@ fn info_plist() -> String { "# ) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A nightly is an app of its own: its name, its identifier (so macOS keeps its permissions apart) and its icon. + #[test] + fn a_nightly_is_an_app_of_its_own() { + let plist = info_plist(&identity(Some("nightly"))); + assert!(plist.contains("Lemon Studio Nightly") && plist.contains("io.github.itsalphaneon.lemonstudio.nightly"), "{plist}"); + for channel in [None, Some("stable")] { + let id = identity(channel); + let plist = info_plist(&id); + assert_eq!(id.icon, "lemonstudio"); + assert!(plist.contains("Lemon Studio") && plist.contains("io.github.itsalphaneon.lemonstudio"), "{plist}"); + } + // Both name the icon in the catalog as macos-icon.sh compiles it. + let nightly = info_plist(&identity(Some("nightly"))); + assert!(nightly.contains("CFBundleIconName\n\tlemonstudio"), "{nightly}"); + } +} diff --git a/tools/logo/gen.py b/tools/logo/gen.py index 7e487e35f..ca8caedba 100644 --- a/tools/logo/gen.py +++ b/tools/logo/gen.py @@ -2,17 +2,18 @@ # SPDX-License-Identifier: GPL-3.0-or-later # This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio -"""Generate the Lemon Studio logo data from crates/os-app/assets/logo/*-lemon.svg. +"""Generate the Lemon Studio logo data from crates/os-app/assets/logo/logo-*-lemon*.svg. python tools/logo/gen.py (run from the repository root) python tools/logo/gen.py --rs (logo.rs only: no Pillow needed) Writes: - crates/os-app/src/logo.rs the full-colour logo's parts (the startup splash) and the greyscale mark (drawn in - one tint), as absolute M/L/Q/C/Z path data + crates/os-app/src/logo.rs the full-colour logo's parts (the startup splash), those the Nightly logo has + instead, and the greyscale mark (drawn in one tint), as absolute M/L/Q/C/Z path data crates/os-app/assets/lemonstudio.ico app icon, 16..256 px (exe, window, taskbar), from the owner's bitmap assets/logo/icon-lemon.png (edited in icon-lemon.pdn, Paint.NET) crates/os-app/assets/lemonstudio.png the same icon at 256 px (Linux: window icon and desktop entry) + crates/os-app/assets/lemonstudio-nightly.{ico,png} the same for nightly builds, from assets/logo/icon-lemon-nightly.png """ import importlib.util import os @@ -29,6 +30,9 @@ # The full-colour logo's parts, back to front: constant name and the id of the path (or of its group) in the SVG. PARTS = [("LABEL", "Label"), ("SHIMMER", "Shimmer"), ("RECORD", "Record"), ("CENTER", "Record_Center"), ("RIND", "Rind")] SLICES = ["Slice_1", "Slice_2", "Slice_3", "Slice_4"] +# The parts only the Nightly logo has, or has differently; the rest it shares with the full-colour logo. +NIGHTLY_PARTS = [("NIGHTLY_CRATERS", "Craters"), ("NIGHTLY_RECORD", "Record"), ("NIGHTLY_STARS", "Stars")] +NIGHTLY_SHARED = ["Label", "Record_Center", "Rind"] + SLICES def layers(svg): @@ -68,6 +72,9 @@ def write_rs(): # The greyscale mark is the coloured one's shapes without the label and the shimmer. shared = [d["RECORD"], d["CENTER"], d["RIND"]] + slices assert [d_string(cmds) for _, cmds in grey] == shared, "the greyscale and full-colour logos differ in shape" + nightly = parts_by_id(os.path.join(ASSETS, "logo", "logo-colored-lemon-nightly.svg")) + for sid in NIGHTLY_SHARED: + assert nightly[sid] == colour[sid], "the Nightly and full-colour logos differ in " + sid lines = tg.HEADER + [ "//! Lemon Studio logo, generated from `assets/logo/logo-colored-lemon.svg` and `logo-greyscale-lemon.svg` by", "//! `tools/logo/gen.py`: do not edit. Absolute M/L/Q/C/Z path data in viewBox units.", @@ -83,6 +90,12 @@ def write_rs(): lines += [' "%s",' % s for s in slices] lines += [ "];", + "", + "// The Nightly logo's own parts (`logo-colored-lemon-nightly.svg`): the moon's craters on the label, the record", + "// without its two windows, and the stars on it. The rest it shares with the full-colour logo.", + ] + lines += ['pub const %s: &str = "%s";' % (name, d_string(tg.convert(nightly[sid]))) for name, sid in NIGHTLY_PARTS] + lines += [ "", "/// The greyscale mark, drawn in one tint (`icons::logo`).", "pub const PATHS: &[&str] = &[RECORD, CENTER, RIND, SLICES[0], SLICES[1], SLICES[2], SLICES[3]];", @@ -91,25 +104,26 @@ def write_rs(): f.write("\n".join(lines) + "\n") -def write_icons(): +def write_icons(bitmap, name): from PIL import Image - src = Image.open(os.path.join(ASSETS, "logo", "icon-lemon.png")).convert("RGBA") + src = Image.open(os.path.join(ASSETS, "logo", bitmap)).convert("RGBA") src = src.crop(src.getchannel("A").getbbox()) # no transparent margin: fill the whole icon cell side = max(src.size) sq = Image.new("RGBA", (side, side), (0, 0, 0, 0)) sq.paste(src, ((side - src.width) // 2, (side - src.height) // 2)) sizes = (256, 128, 64, 48, 40, 32, 24, 20, 16) imgs = [sq.resize((s, s), Image.LANCZOS) for s in sizes] - imgs[0].save(os.path.join(ASSETS, "lemonstudio.ico"), sizes=[(s, s) for s in sizes], append_images=imgs[1:]) - # Linux: the freedesktop icon named after the app id (Wayland compositors show it through lemonstudio.desktop). - imgs[0].save(os.path.join(ASSETS, "lemonstudio.png")) + imgs[0].save(os.path.join(ASSETS, name + ".ico"), sizes=[(s, s) for s in sizes], append_images=imgs[1:]) + # Linux: the freedesktop icon named after the app id (Wayland compositors show it through the desktop entry). + imgs[0].save(os.path.join(ASSETS, name + ".png")) def main(): write_rs() if "--rs" not in sys.argv[1:]: - write_icons() + write_icons("icon-lemon.png", "lemonstudio") + write_icons("icon-lemon-nightly.png", "lemonstudio-nightly") if __name__ == "__main__": diff --git a/tools/logo/macos-icon.sh b/tools/logo/macos-icon.sh index b0a9e4f10..ac6fd107f 100755 --- a/tools/logo/macos-icon.sh +++ b/tools/logo/macos-icon.sh @@ -3,11 +3,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later # This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio -# Generate the macOS app icon from the Icon Composer document crates/os-app/assets/logo/macOS.icon: +# Generate the macOS app icon from the Icon Composer document crates/os-app/assets/logo/macOS.icon, or for nightly +# builds from macOS-nightly.icon: # -# tools/logo/macos-icon.sh (on macOS, with Xcode and Icon Composer in /Applications; run from anywhere) +# tools/logo/macos-icon.sh [nightly] (on macOS, with Xcode and Icon Composer in /Applications; run from anywhere) # -# Writes, for `cargo run -p bundle-app` to put in Lemon Studio.app: +# Writes, for `cargo run -p bundle-app` to put in the app (nightly: lemonstudio-nightly.car and .icns): # crates/os-app/assets/lemonstudio.car the compiled asset catalog (Assets.car in the bundle): the icon as macOS 26 # and later draw it, light, dark, tinted and clear (Xcode's actool) # crates/os-app/assets/lemonstudio.icns every size an .icns holds, the Default rendition, 8 bits per channel in @@ -19,18 +20,24 @@ root=$(cd "$(dirname "$0")/../.." && pwd) ictool="${ICTOOL:-/Applications/Icon Composer.app/Contents/Executables/ictool}" # Xcode's tools, whatever `xcode-select` points at (the Command Line Tools have no actool). export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}" -src="$root/crates/os-app/assets/logo/macOS.icon" assets="$root/crates/os-app/assets" +if [ "${1:-}" = nightly ]; then + src="$assets/logo/macOS-nightly.icon" + out="lemonstudio-nightly" +else + src="$assets/logo/macOS.icon" + out="lemonstudio" +fi work=$(mktemp -d) trap 'rm -rf "$work"' EXIT # The asset catalog. The icon's name in it is its document's: compiled as lemonstudio.icon, it is `lemonstudio` -# (`CFBundleIconName` in the bundle's Info.plist). +# (`CFBundleIconName` in the bundle's Info.plist), a nightly's too. cp -R "$src" "$work/lemonstudio.icon" mkdir "$work/car" xcrun actool "$work/lemonstudio.icon" --compile "$work/car" --platform macosx --minimum-deployment-target 11.0 \ --app-icon lemonstudio --output-partial-info-plist "$work/car/partial.plist" --errors --warnings > /dev/null -cp "$work/car/Assets.car" "$assets/lemonstudio.car" +cp "$work/car/Assets.car" "$assets/$out.car" # The .icns: icon_x[@2x].png, the image at that size and scale. set_dir="$work/lemonstudio.iconset" @@ -60,6 +67,6 @@ for path in CommandLine.arguments.dropFirst() { } SWIFT xcrun swift "$work/to8.swift" "$set_dir"/*.png -iconutil -c icns -o "$assets/lemonstudio.icns" "$set_dir" -echo "$assets/lemonstudio.car" -echo "$assets/lemonstudio.icns" +iconutil -c icns -o "$assets/$out.icns" "$set_dir" +echo "$assets/$out.car" +echo "$assets/$out.icns" diff --git a/tools/package/appimage.sh b/tools/package/appimage.sh new file mode 100755 index 000000000..1250a3cfc --- /dev/null +++ b/tools/package/appimage.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 ItsAlphaNeon +# SPDX-License-Identifier: GPL-3.0-or-later +# This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +# Packs a release build of Lemon Studio as an AppImage (the release workflow's Linux download): +# +# tools/package/appimage.sh [nightly] +# +# The AppDir holds the program, its desktop entry (crates/os-app/assets/lemonstudio.desktop, with the version added) +# and its icon. A nightly's is named and looks its own (lemonstudio-nightly, as the program's app id says), so that it +# sits beside Lemon Studio on a desktop. No libraries go in: the program links only the C and C++ runtimes and ALSA, which every desktop +# distribution has, and loads X11, Wayland and JACK itself when they are there. It is built on an older Ubuntu than CI +# uses so that those runtimes are old enough for the distributions in use. +# +# Needs appimagetool: on the PATH, or named by APPIMAGETOOL (the workflow downloads it). The AppImage runtime it adds +# is the static one, which needs no FUSE library on the user's system. +set -euo pipefail + +if [ $# -lt 3 ] || [ $# -gt 4 ]; then + echo "usage: $0 [nightly]" >&2 + exit 2 +fi +bin=$1 +version=$2 +out=$3 +if [ "${4:-}" = nightly ]; then + id=lemonstudio-nightly + name="Lemon Studio Nightly" +else + id=lemonstudio + name="Lemon Studio" +fi +root=$(cd "$(dirname "$0")/../.." && pwd) +assets=$root/crates/os-app/assets + +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +appdir=$work/LemonStudio.AppDir +mkdir -p "$appdir/usr/bin" "$appdir/usr/share/applications" "$appdir/usr/share/icons/hicolor/256x256/apps" + +install -m755 "$bin" "$appdir/usr/bin/lemonstudio" +install -m644 "$assets/$id.png" "$appdir/usr/share/icons/hicolor/256x256/apps/$id.png" +install -m644 "$assets/$id.png" "$appdir/$id.png" +ln -s "$id.png" "$appdir/.DirIcon" +sed -e "/^\[Desktop Entry\]/a X-AppImage-Version=$version" -e "s/^Name=.*/Name=$name/" -e "s/^Icon=.*/Icon=$id/" \ + -e "s/^StartupWMClass=.*/StartupWMClass=$id/" "$assets/lemonstudio.desktop" >"$appdir/$id.desktop" +cp "$appdir/$id.desktop" "$appdir/usr/share/applications/$id.desktop" + +# The AppImage runtime sets APPIMAGE to the AppImage's own path, which is what Lemon Studio's updater replaces. +cat >"$appdir/AppRun" <<'APPRUN' +#!/bin/sh +here="$(dirname "$(readlink -f "$0")")" +exec "$here/usr/bin/lemonstudio" "$@" +APPRUN +chmod 755 "$appdir/AppRun" + +ARCH=$(uname -m) "${APPIMAGETOOL:-appimagetool}" --no-appstream "$appdir" "$out" +echo "$out" diff --git a/tools/package/manifest.py b/tools/package/manifest.py new file mode 100644 index 000000000..fffba8664 --- /dev/null +++ b/tools/package/manifest.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 ItsAlphaNeon +# SPDX-License-Identifier: GPL-3.0-or-later +# This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +"""Writes a release channel's manifest (`nightly.json` or `stable.json`) for Lemon Studio's updater +(`tools/updater`, whose `manifest` module reads it): the release's version, commit and changes, and per platform the +program to download, with its size and SHA-256. The release workflow signs it with minisign. + + manifest.py --channel nightly --version 2026.11.7.1 --commit --repo owner/name --tag nightly-2026.11.7.1 \\ + --notes generated-notes.md --dist dist --out dist/nightly.json + +The programs are found in `dist` by name (`programs`), per platform as Rust names it. The plugin downloads +(`LemonStudio-Plugins-...`) are not programs to update and are left out. The notes are GitHub's generated release +notes, one pull request a line. +""" + +import argparse +import datetime +import hashlib +import json +import os +import re +import sys + + +def notes(markdown): + """The pull requests' titles in GitHub's generated notes ("* Title by @someone in https://.../pull/1"), from their + "What's Changed" section (not "New Contributors").""" + out = [] + listing = True + for line in markdown.splitlines(): + if line.startswith("#"): + listing = "what's changed" in line.lower() + continue + m = re.match(r"^\s*[*-]\s+(.+?)(?:\s+by\s+@\S+)?(?:\s+in\s+https://\S+)?\s*$", line) + if listing and m: + out.append(m.group(1)) + return out + + +def programs(channel, version): + """The file holding the program, per platform. The `.exe` and the AppImage carry no version: an update replaces the + file in place, so a shortcut to it never breaks. A nightly's have a name of their own, so that a nightly and a + tagged release can sit in one folder. macOS's zip is only unpacked; the `.app` inside has a fixed name.""" + stem = "LemonStudioNightly" if channel == "nightly" else "LemonStudio" + return { + "windows-x86_64": f"{stem}.exe", + "linux-x86_64": f"{stem}.AppImage", + "macos-aarch64": f"LemonStudio-{version}-macos-aarch64.zip", + } + + +def assets(dist, channel, version, base): + found = {} + for platform, name in programs(channel, version).items(): + path = os.path.join(dist, name) + if not os.path.isfile(path): + continue + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + found[platform] = {"url": f"{base}/{name}", "size": os.path.getsize(path), "sha256": h.hexdigest()} + return found + + +def main(): + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + for arg in ("channel", "version", "commit", "repo", "tag", "notes", "dist", "out"): + p.add_argument("--" + arg, required=True) + a = p.parse_args() + if a.channel not in ("nightly", "stable"): + sys.exit(f"unknown channel {a.channel!r}") + with open(a.notes, encoding="utf-8") as f: + changes = notes(f.read()) + page = f"https://github.com/{a.repo}/releases/tag/{a.tag}" + found = assets(a.dist, a.channel, a.version, f"https://github.com/{a.repo}/releases/download/{a.tag}") + if not found: + sys.exit(f"none of the programs in {a.dist}: {', '.join(programs(a.channel, a.version).values())}") + manifest = { + "channel": a.channel, + "version": a.version, + "commit": a.commit, + "date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "page": page, + "notes": changes, + "assets": found, + } + with open(a.out, "w", encoding="utf-8", newline="\n") as f: + json.dump(manifest, f, indent=2) + f.write("\n") + print(f"{a.out}: {a.channel} {a.version}, {', '.join(found)}; {len(changes)} changes") + + +if __name__ == "__main__": + main() diff --git a/tools/package/plan.sh b/tools/package/plan.sh new file mode 100755 index 000000000..54e6ededa --- /dev/null +++ b/tools/package/plan.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 ItsAlphaNeon +# SPDX-License-Identifier: GPL-3.0-or-later +# This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +# Works out what the release workflow is building: the channel, version and tag (docs/releasing.md), and whether there +# is anything to build. Run from a checkout with its history and tags. Reads, as GitHub Actions sets them: +# +# GITHUB_REF, GITHUB_REF_NAME what the run is on: a tag vMAJOR.MINOR.PATCH, or main for a nightly +# FORCE=true a nightly even if main has not moved +# DRY_RUN=true a rehearsal: builds but publishes nothing, so it may run on any branch +# REHEARSE_TAG=v1.2.3 with DRY_RUN: plan as that tag, which need not exist yet +# +# Prints what it decided, and writes channel, version, tag, previous, sha and skip to $GITHUB_OUTPUT. +set -euo pipefail + +ref=${GITHUB_REF:-} +name=${GITHUB_REF_NAME:-} +if [ -n "${REHEARSE_TAG:-}" ]; then + if [ "${DRY_RUN:-}" != true ]; then + echo "::error::A tag can only be rehearsed in a dry run: push the tag to release it" + exit 1 + fi + ref=refs/tags/$REHEARSE_TAG + name=$REHEARSE_TAG +fi + +sha=$(git rev-parse HEAD) +skip=false +if [[ "$ref" == refs/tags/* ]]; then + channel=stable + tag=$name + if ! [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::$tag is not a release tag: they are vMAJOR.MINOR.PATCH" + exit 1 + fi + version=${tag#v} + workspace=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml | tr -d '\r') + if [ "$version" != "$workspace" ]; then + echo "::error::The tag $tag does not match the workspace version $workspace in Cargo.toml" + exit 1 + fi + # A release is of something that landed. A tag is easily pushed from the wrong branch, and there is no taking back + # what installed copies have downloaded. + if [ "${DRY_RUN:-}" != true ] && ! git merge-base --is-ancestor "$sha" origin/main; then + echo "::error::$tag is not on main" + exit 1 + fi + previous=$( (git tag -l 'v*.*.*' --sort=-v:refname | grep -vx "$tag" || true) | head -n1) +else + if [ "$ref" != refs/heads/main ] && [ "${DRY_RUN:-}" != true ]; then + echo "::error::Nightlies are built from main (this run is on $ref)" + exit 1 + fi + channel=nightly + day=$(date -u +%Y.%-m.%-d) + n=$(( $(git tag -l "nightly-$day.*" | wc -l) + 1 )) + version=$day.$n + tag=nightly-$version + previous=$(git tag -l 'nightly-*.*.*.*' --sort=-v:refname | head -n1) + if [ -n "$previous" ] && [ "$(git rev-list -n1 "$previous")" = "$sha" ] && [ "${FORCE:-}" != true ] && [ "${DRY_RUN:-}" != true ]; then + echo "main has not changed since $previous: no nightly" + skip=true + fi +fi + +echo "$channel $version ($tag at $sha), after ${previous:-nothing}${DRY_RUN:+, dry run}" +{ + echo "channel=$channel" + echo "version=$version" + echo "tag=$tag" + echo "previous=$previous" + echo "sha=$sha" + echo "skip=$skip" +} >>"${GITHUB_OUTPUT:-/dev/null}" diff --git a/tools/package/plugins-readme.txt b/tools/package/plugins-readme.txt new file mode 100644 index 000000000..8ea9769b5 --- /dev/null +++ b/tools/package/plugins-readme.txt @@ -0,0 +1,26 @@ +Lemon Studio's plugins, as VST3 and CLAP plugins for other DAWs +================================================================= + +Lemon Studio itself has these plugins built in; this download is for other DAWs. +Copy the contents of the VST3 folder to your VST3 plugin folder, and the contents of +the CLAP folder to your CLAP plugin folder, then let your DAW scan for plugins. + +Windows + VST3: %LOCALAPPDATA%\Programs\Common\VST3 (for you only) + C:\Program Files\Common Files\VST3 (for everyone; needs an administrator) + CLAP: %LOCALAPPDATA%\Programs\Common\CLAP + C:\Program Files\Common Files\CLAP + +Linux + VST3: ~/.vst3 + CLAP: ~/.clap + +macOS + VST3: ~/Library/Audio/Plug-Ins/VST3 + CLAP: ~/Library/Audio/Plug-Ins/CLAP + The plugins are not notarized by Apple. After copying them, run in Terminal: + xattr -dr com.apple.quarantine ~/Library/Audio/Plug-Ins/VST3/*.vst3 ~/Library/Audio/Plug-Ins/CLAP/*.clap + or DAWs won't load them. + +Lemon Studio is free software under the GNU General Public License, version 3 or later: +https://github.com/ItsAlphaNeon/Lemon-Studio diff --git a/tools/package/release-notes.md b/tools/package/release-notes.md new file mode 100644 index 000000000..a05a324c9 --- /dev/null +++ b/tools/package/release-notes.md @@ -0,0 +1,28 @@ +{INTRO} + +### Downloads + +| | Lemon Studio | Its plugins for other DAWs (VST3 and CLAP) | +|---|---|---| +| Windows | `{PROGRAM}.exe` | `LemonStudio-Plugins-{VERSION}-windows-x86_64.zip` | +| Linux | `{PROGRAM}.AppImage` | `LemonStudio-Plugins-{VERSION}-linux-x86_64.zip` | +| macOS (Apple silicon) | `LemonStudio-{VERSION}-macos-aarch64.zip` | `LemonStudio-Plugins-{VERSION}-macos-aarch64.zip` | + +Lemon Studio has its own plugins built in; the plugin downloads are for using them in other DAWs. Once installed, Lemon +Studio checks for updates when it starts (Settings ▸ General ▸ Updates) and installs them itself, under the same file +name, so shortcuts to it keep working. + +- **Windows:** run the `.exe`. It isn't signed, so SmartScreen may say it doesn't recognise it: choose **More info** ▸ + **Run anyway**. +- **Linux:** make the AppImage executable (`chmod +x {PROGRAM}.AppImage`, or in its file properties) and run it. +- **macOS:** Lemon Studio is not notarized by Apple (that needs a paid Apple Developer account the project doesn't have), + so macOS stops it the first time. Unzip it and move **{APP}.app** to Applications. Then open it once, go to + **System Settings ▸ Privacy & Security** and choose **Open Anyway** (on macOS 14 and earlier, right-click the app ▸ + **Open** ▸ **Open** also works). Or, in Terminal: + `xattr -dr com.apple.quarantine "/Applications/{APP}.app"`. Updates Lemon Studio installs itself don't ask + again. +- **Plugins:** unzip and copy the `VST3` folder's contents to your VST3 folder and the `CLAP` folder's to your CLAP + folder (the zip's `README.txt` lists where those are). On macOS, also run + `xattr -dr com.apple.quarantine` on the copied plugins, or DAWs won't load them. + +{CHANGES} diff --git a/tools/package/test.sh b/tools/package/test.sh new file mode 100755 index 000000000..44c02b51c --- /dev/null +++ b/tools/package/test.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# Copyright (C) 2026 ItsAlphaNeon +# SPDX-License-Identifier: GPL-3.0-or-later +# This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +# Tests the release workflow's scripts without building or publishing anything (CI runs it on Linux): +# +# tools/package/test.sh +# +# plan.sh against a throwaway repository, and manifest.py against empty files with release names. +set -euo pipefail + +here=$(cd "$(dirname "$0")" && pwd) +plan=$here/plan.sh +manifest=$here/manifest.py +# Whichever runs: on Windows a python3 can be the shortcut to the Microsoft Store. +for python in python3 python; do + "$python" -c '' 2>/dev/null && break +done +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +fails=0 + +# Runs plan.sh with the given VAR=value settings; prints its outputs on one line, "failed" if it refused. +run() { + local out=$work/out + : >"$out" + if (cd "$work/repo" && env -u FORCE -u DRY_RUN -u REHEARSE_TAG GITHUB_OUTPUT="$out" "$@" bash "$plan" >/dev/null 2>&1); then + tr -d '\r' <"$out" | grep -E '^(channel|version|tag|previous|skip)=' | sed 's/^[a-z]*=//' | paste -sd' ' - + else + echo failed + fi +} + +expect() { + local what=$1 want=$2 + shift 2 + local got + got=$(run "$@") + if [ "$got" = "$want" ]; then + echo "ok $what" + else + echo "FAIL $what" + echo " want: $want" + echo " got: $got" + fails=$((fails + 1)) + fi +} + +git init -q -b main "$work/repo" +cd "$work/repo" +git config user.name test +git config user.email test@example.invalid +git config commit.gpgsign false +git config tag.gpgsign false +printf '[workspace]\nmembers = []\n\n[workspace.package]\nversion = "0.1.0"\nedition = "2024"\n\n[workspace.dependencies]\n' >Cargo.toml +git add Cargo.toml +git commit -qm one +git update-ref refs/remotes/origin/main HEAD +day=$(date -u +%Y.%-m.%-d) +main="GITHUB_REF=refs/heads/main GITHUB_REF_NAME=main" +tag() { echo "GITHUB_REF=refs/tags/$1 GITHUB_REF_NAME=$1"; } + +# shellcheck disable=SC2086 # the settings are several words on purpose +{ + expect "the first nightly" "nightly $day.1 nightly-$day.1 false" $main + expect "a nightly only from main" failed GITHUB_REF=refs/heads/other GITHUB_REF_NAME=other + expect "a rehearsal from any branch" "nightly $day.1 nightly-$day.1 false" GITHUB_REF=refs/heads/other GITHUB_REF_NAME=other DRY_RUN=true + + git tag "nightly-$day.1" + expect "main has not moved: no nightly" "nightly $day.2 nightly-$day.2 nightly-$day.1 true" $main + expect "main has not moved: forced" "nightly $day.2 nightly-$day.2 nightly-$day.1 false" $main FORCE=true + expect "main has not moved: rehearsed" "nightly $day.2 nightly-$day.2 nightly-$day.1 false" $main DRY_RUN=true + + git commit -q --allow-empty -m two + git update-ref refs/remotes/origin/main HEAD + expect "main moved: a second nightly today" "nightly $day.2 nightly-$day.2 nightly-$day.1 false" $main + + expect "the first release" "stable 0.1.0 v0.1.0 false" $(tag v0.1.0) + expect "a tag must match Cargo.toml" failed $(tag v0.2.0) + expect "alpha is not in the tag" failed $(tag v0.1.0-alpha.1) + expect "a tag has three numbers" failed $(tag v0.1) + + git tag v0.0.9 + expect "a release after an earlier one" "stable 0.1.0 v0.1.0 v0.0.9 false" $(tag v0.1.0) + git tag v0.1.0 + expect "the tag itself is not its previous" "stable 0.1.0 v0.1.0 v0.0.9 false" $(tag v0.1.0) + git tag -d v0.1.0 >/dev/null + + expect "a tag rehearsed" "stable 0.1.0 v0.1.0 v0.0.9 false" $main DRY_RUN=true REHEARSE_TAG=v0.1.0 + expect "a rehearsal is only a rehearsal" failed $main REHEARSE_TAG=v0.1.0 + + git switch -q -c elsewhere + git commit -q --allow-empty -m three + expect "a tag off main is refused" failed $(tag v0.1.0) + expect "but can be rehearsed" "stable 0.1.0 v0.1.0 v0.0.9 false" $(tag v0.1.0) DRY_RUN=true +} + +# The manifest: the app's three programs, not the plugin zips or anything else. +mkdir "$work/dist" +v=0.1.0 +for f in LemonStudio.exe LemonStudio.AppImage "LemonStudio-$v-macos-aarch64.zip" \ + LemonStudioNightly.exe LemonStudioNightly.AppImage "LemonStudio-2026.1.1.1-macos-aarch64.zip" \ + "LemonStudio-Plugins-$v-windows-x86_64.zip" LICENSE; do + : >"$work/dist/$f" +done +printf "## What's Changed\n* Mixer: a fix by @someone in https://github.com/o/r/pull/1\n* Another\n\n## New Contributors\n* @someone made their first contribution\n" >"$work/changes.md" +"$python" "$manifest" --channel stable --version "$v" --commit abc --repo o/r --tag "v$v" --notes "$work/changes.md" --dist "$work/dist" --out "$work/stable.json" >/dev/null +if "$python" - "$work/stable.json" <<'PY' +import json, sys +m = json.load(open(sys.argv[1])) +assert sorted(m["assets"]) == ["linux-x86_64", "macos-aarch64", "windows-x86_64"], m["assets"] +assert m["notes"] == ["Mixer: a fix", "Another"], m["notes"] +assert m["assets"]["linux-x86_64"]["url"] == "https://github.com/o/r/releases/download/v0.1.0/LemonStudio.AppImage" +assert m["assets"]["windows-x86_64"]["url"] == "https://github.com/o/r/releases/download/v0.1.0/LemonStudio.exe" +assert m["assets"]["macos-aarch64"]["url"] == "https://github.com/o/r/releases/download/v0.1.0/LemonStudio-0.1.0-macos-aarch64.zip" +assert m["page"] == "https://github.com/o/r/releases/tag/v0.1.0" +PY +then + echo "ok the manifest" +else + echo "FAIL the manifest" + fails=$((fails + 1)) +fi +"$python" "$manifest" --channel nightly --version 2026.1.1.1 --commit abc --repo o/r --tag nightly-2026.1.1.1 --notes "$work/changes.md" --dist "$work/dist" --out "$work/nightly.json" >/dev/null +if "$python" - "$work/nightly.json" <<'PY' +import json, sys +a = json.load(open(sys.argv[1]))["assets"] +base = "https://github.com/o/r/releases/download/nightly-2026.1.1.1/" +assert a["windows-x86_64"]["url"] == base + "LemonStudioNightly.exe", a +assert a["linux-x86_64"]["url"] == base + "LemonStudioNightly.AppImage", a +assert a["macos-aarch64"]["url"] == base + "LemonStudio-2026.1.1.1-macos-aarch64.zip", a +PY +then + echo "ok a nightly's programs have their own names" +else + echo "FAIL a nightly's programs have their own names" + fails=$((fails + 1)) +fi +echo "The first build on this channel." >"$work/first.md" +"$python" "$manifest" --channel stable --version "$v" --commit abc --repo o/r --tag "v$v" --notes "$work/first.md" --dist "$work/dist" --out "$work/first.json" >/dev/null +if "$python" -c 'import json, sys; sys.exit(json.load(open(sys.argv[1]))["notes"] != [])' "$work/first.json"; then + echo "ok the first build lists no changes" +else + echo "FAIL the first build lists no changes" + fails=$((fails + 1)) +fi + +if [ "$fails" -ne 0 ]; then + echo "$fails failed" + exit 1 +fi +echo "all passed" diff --git a/tools/updater/Cargo.toml b/tools/updater/Cargo.toml new file mode 100644 index 000000000..83dc9cfbc --- /dev/null +++ b/tools/updater/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "updater" +description = "Lemon Studio's updater: checks a release channel for a newer build, downloads it and puts it in place" +version.workspace = true +edition.workspace = true +license.workspace = true + +[[bin]] +name = "lemonstudio-updater" +path = "src/main.rs" + +[dependencies] +# HTTPS through rustls with ring (no C toolchain beyond what ring builds itself, no CMake), checked against the system's +# own certificate store (read with rustls-native-certs) rather than a bundled list. +ureq = { version = "3", default-features = false, features = ["rustls-no-provider", "gzip"] } +rustls-native-certs = "0.8" +rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12"] } +minisign-verify = "0.2" +sha2 = "0.10" +semver = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Threading"] } diff --git a/tools/updater/src/main.rs b/tools/updater/src/main.rs new file mode 100644 index 000000000..8bd811ac9 --- /dev/null +++ b/tools/updater/src/main.rs @@ -0,0 +1,202 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! `lemonstudio-updater`: Lemon Studio's updater, part of the builds the release workflow makes (nightly and stable). +//! A build from source has none: Lemon Studio then never checks for updates (`os-app`'s `update`). +//! +//! Lemon Studio carries this program inside itself, writes it to its user folder and starts it there (so it can +//! replace the program it came from). It has no window; it answers on standard output, one JSON object per line, each +//! with a `status`: +//! +//! - `check --channel --current `: is there a newer build? +//! `none` (the channel has no release yet), `current`, `unsupported` (no build for this platform), +//! `available` (`version`, `notes`, `page`, `size`), or `error` (`message`). +//! - `download --channel --current --to `: as `check`, then downloads the newer build into `folder` +//! (a download already there for the same build is kept), reporting `progress` (`done`, `total`) and then `staged` +//! (`version`, `path`, `notes`, `page`). +//! - `apply --pid --staged --target --version --result [--relaunch]`: waits for Lemon +//! Studio (process `n`) to close, puts the staged build in place of `target` (`swap`), writes the outcome to `file` +//! (`{"ok": bool, "version": "...", "message": "..."}`) for the next start to report, and starts Lemon Studio again +//! if asked to (the old one, should the update have failed). +//! +//! `--base ` replaces the releases URL (for testing against another server). Every manifest must carry a valid +//! minisign signature by the release key, whose public half is built in (`LEMON_UPDATE_KEY` at build time): without +//! it, this program checks nothing. + +// No console window when Lemon Studio starts it; its answers go through the pipes Lemon Studio gives it. +#![cfg_attr(windows, windows_subsystem = "windows")] + +mod manifest; +mod net; +mod swap; + +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use serde_json::json; + +/// Where the releases are. +const RELEASES: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio/releases"; + +/// The release key's public half (minisign), set by the release workflow from the repository's `MINISIGN_PUBLIC_KEY`. +const KEY: Option<&str> = option_env!("LEMON_UPDATE_KEY"); + +/// How long `apply` waits for Lemon Studio to close (its plugins may take a while to shut down). +const CLOSE_TIMEOUT: Duration = Duration::from_secs(300); + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + let code = match run(&args) { + Ok(()) => 0, + Err(message) => { + emit(json!({ "status": "error", "message": message })); + 1 + } + }; + std::process::exit(code); +} + +fn run(args: &[String]) -> Result<(), String> { + let cmd = args.first().map(String::as_str).unwrap_or(""); + let opts = Opts(&args[args.len().min(1)..]); + match cmd { + "check" => check(&opts, None), + "download" => check(&opts, Some(PathBuf::from(opts.need("--to")?))), + "apply" => apply(&opts), + "--version" => { + println!("lemonstudio-updater {}", env!("CARGO_PKG_VERSION")); + Ok(()) + } + _ => Err(format!("usage: lemonstudio-updater check|download|apply ... (got {cmd:?})")), + } +} + +/// `--name value` options. +struct Opts<'a>(&'a [String]); + +impl Opts<'_> { + fn get(&self, name: &str) -> Option<&str> { + self.0.iter().position(|a| a == name).and_then(|i| self.0.get(i + 1)).map(String::as_str) + } + + fn need(&self, name: &str) -> Result<&str, String> { + self.get(name).ok_or_else(|| format!("{name} is missing")) + } + + fn flag(&self, name: &str) -> bool { + self.0.iter().any(|a| a == name) + } +} + +fn emit(v: serde_json::Value) { + use std::io::Write; + let mut out = std::io::stdout().lock(); + let _ = writeln!(out, "{v}"); + let _ = out.flush(); +} + +/// `check`, and `download` when `to` is given. +fn check(opts: &Opts, to: Option) -> Result<(), String> { + let channel = opts.need("--channel")?; + let current = opts.need("--current")?; + let base = opts.get("--base").unwrap_or(RELEASES); + let key = KEY.filter(|k| !k.trim().is_empty()).ok_or("this updater was built without the release key")?; + let url = manifest::url(base, channel)?; + let net = net::Net::new()?; + let Some(body) = net.get(&url)? else { + emit(json!({ "status": "none" })); + return Ok(()); + }; + let sig = net.get(&format!("{url}.minisig"))?.ok_or("the manifest has no signature")?; + let m = manifest::verified(&body, &String::from_utf8_lossy(&sig), key, channel)?; + if !manifest::newer(channel, &m.version, current)? { + emit(json!({ "status": "current", "version": m.version })); + return Ok(()); + } + let Some(asset) = m.assets.get(&manifest::platform()) else { + emit(json!({ "status": "unsupported", "version": m.version, "platform": manifest::platform() })); + return Ok(()); + }; + let Some(to) = to else { + emit(json!({ "status": "available", "version": m.version, "notes": m.notes, "page": m.page, "size": asset.size })); + return Ok(()); + }; + let path = stage(&net, &m, asset, &to)?; + emit(json!({ "status": "staged", "version": m.version, "path": path, "notes": m.notes, "page": m.page })); + Ok(()) +} + +/// What a finished download in the staging folder is (`staged.json`). +#[derive(serde::Serialize, serde::Deserialize)] +struct Staged { + version: String, + sha256: String, + path: PathBuf, +} + +/// Downloads `asset` into `to` (emptied first, unless it holds this very build already) and unpacks a `.zip` (macOS: +/// the `.app`); the path of the build to install. +fn stage(net: &net::Net, m: &manifest::Manifest, asset: &manifest::Asset, to: &Path) -> Result { + let record = to.join("staged.json"); + if let Some(s) = std::fs::read(&record).ok().and_then(|b| serde_json::from_slice::(&b).ok()) { + if s.version == m.version && s.sha256.eq_ignore_ascii_case(&asset.sha256) && s.path.exists() { + return Ok(s.path); + } + } + let _ = std::fs::remove_dir_all(to); + std::fs::create_dir_all(to).map_err(|e| format!("{}: {e}", to.display()))?; + let name = asset.url.rsplit('/').next().filter(|n| !n.is_empty() && !n.contains(['\\', ':'])).unwrap_or("download"); + let file = to.join(name); + net.download(&asset.url, &file, asset.size, &asset.sha256, |done, total| { + emit(json!({ "status": "progress", "done": done, "total": total })); + })?; + let path = if name.ends_with(".zip") { unzip_app(&file, &to.join("unpacked"))? } else { file }; + let s = Staged { version: m.version.clone(), sha256: asset.sha256.clone(), path: path.clone() }; + std::fs::write(&record, serde_json::to_vec(&s).expect("json")).map_err(|e| format!("{}: {e}", record.display()))?; + Ok(path) +} + +/// macOS: the `.app` inside `zip`, unpacked into `dir` with `ditto` (as the Finder does: links and signature kept). +fn unzip_app(zip: &Path, dir: &Path) -> Result { + let ok = std::process::Command::new("ditto").args(["-x", "-k"]).arg(zip).arg(dir).status().is_ok_and(|s| s.success()); + if !ok { + return Err(format!("could not unpack {}", zip.display())); + } + let _ = std::fs::remove_file(zip); + std::fs::read_dir(dir) + .map_err(|e| format!("{}: {e}", dir.display()))? + .flatten() + .map(|e| e.path()) + .find(|p| p.extension().is_some_and(|x| x == "app")) + .ok_or_else(|| format!("{} holds no .app", zip.display())) +} + +fn apply(opts: &Opts) -> Result<(), String> { + let pid: u32 = opts.need("--pid")?.parse().map_err(|_| "--pid is not a number")?; + let staged = PathBuf::from(opts.need("--staged")?); + let target = PathBuf::from(opts.need("--target")?); + let version = opts.need("--version")?; + let result = PathBuf::from(opts.need("--result")?); + let outcome = if swap::wait_for_exit(pid, CLOSE_TIMEOUT) { + swap::replace(&staged, &target) + } else { + Err("Lemon Studio did not close".into()) + }; + if outcome.is_ok() { + // The download is not needed any more (`staged` is inside the staging folder; its record too). + if let Some(dir) = staged.ancestors().find(|d| d.join("staged.json").exists()) { + let _ = std::fs::remove_dir_all(dir); + } + } + let message = outcome.as_ref().err().cloned().unwrap_or_default(); + let record = json!({ "ok": outcome.is_ok(), "version": version, "message": message }); + if let Some(dir) = result.parent() { + let _ = std::fs::create_dir_all(dir); + } + let _ = std::fs::write(&result, record.to_string()); + if opts.flag("--relaunch") { + swap::relaunch(&target)?; + } + outcome +} diff --git a/tools/updater/src/manifest.rs b/tools/updater/src/manifest.rs new file mode 100644 index 000000000..2d6c2a943 --- /dev/null +++ b/tools/updater/src/manifest.rs @@ -0,0 +1,185 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! A release channel's manifest: `nightly.json` or `stable.json`, written and signed by the release workflow +//! (`.github/workflows/release.yml`), signed with minisign. It names the channel's newest build and, per platform, the +//! file to download with its size and SHA-256. +//! +//! ```json +//! { "channel": "nightly", "version": "2026.11.7.1", "commit": "0be813c5…", "date": "2026-11-07T03:12:44Z", +//! "page": "https://github.com/ItsAlphaNeon/Lemon-Studio/releases/tag/nightly-2026.11.7.1", +//! "notes": ["Linux: typed notes end when their key is up", "…"], +//! "assets": { "windows-x86_64": { "url": "https://…", "size": 52183040, "sha256": "9f2c…" }, … } } +//! ``` +//! +//! Versions: a nightly is dated, `YEAR.MONTH.DAY.N` (UTC, no zero padding; `N` counts that day's nightlies from 1); a +//! tagged release (`stable`: an alpha while its major version is 0) is semantic, `MAJOR.MINOR.PATCH`. The two are never +//! compared with each other. + +use std::cmp::Ordering; +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +pub const NIGHTLY: &str = "nightly"; +pub const STABLE: &str = "stable"; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Manifest { + pub channel: String, + pub version: String, + #[serde(default)] + pub commit: String, + #[serde(default)] + pub date: String, + /// The release's page on GitHub. + #[serde(default)] + pub page: String, + /// What changed, one line each (pull request titles). + #[serde(default)] + pub notes: Vec, + /// By platform (`platform()`). + pub assets: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Asset { + pub url: String, + pub size: u64, + /// Lowercase hex. + pub sha256: String, +} + +/// This build's platform, as the manifest's `assets` name it: `windows-x86_64`, `linux-x86_64`, `macos-aarch64`, ... +pub fn platform() -> String { + format!("{}-{}", std::env::consts::OS, std::env::consts::ARCH) +} + +/// Where a channel's manifest is, under the releases URL `base` (`https://github.com///releases`). Stable: +/// the newest release that is not a pre-release (GitHub's `latest`). Nightly: the `nightly` pre-release, which only +/// holds the manifest and is moved on by every nightly. +pub fn url(base: &str, channel: &str) -> Result { + let base = base.trim_end_matches('/'); + match channel { + STABLE => Ok(format!("{base}/latest/download/{STABLE}.json")), + NIGHTLY => Ok(format!("{base}/download/{NIGHTLY}/{NIGHTLY}.json")), + _ => Err(format!("unknown channel {channel:?}")), + } +} + +/// The manifest in `body`, once its minisign signature `sig` checks out against the public key `key` (base64, the last +/// line of a `minisign.pub`), and it is for `channel`. +pub fn verified(body: &[u8], sig: &str, key: &str, channel: &str) -> Result { + let key = key.lines().map(str::trim).filter(|l| !l.is_empty()).last().unwrap_or(""); + let pk = minisign_verify::PublicKey::from_base64(key).map_err(|e| format!("update key: {e}"))?; + let sig = minisign_verify::Signature::decode(sig).map_err(|e| format!("manifest signature: {e}"))?; + pk.verify(body, &sig, false).map_err(|e| format!("manifest signature does not match: {e}"))?; + let m: Manifest = serde_json::from_slice(body).map_err(|e| format!("manifest: {e}"))?; + if m.channel != channel { + return Err(format!("the manifest is for the {} channel, not {channel}", m.channel)); + } + Ok(m) +} + +/// A nightly's dated version, `YEAR.MONTH.DAY.N`. +fn dated(v: &str) -> Option<[u32; 4]> { + let parts: Vec = v.split('.').map(|p| p.parse().ok()).collect::>()?; + let [y, m, d, n] = parts[..] else { return None }; + ((1..=12).contains(&m) && (1..=31).contains(&d) && n >= 1).then_some([y, m, d, n]) +} + +/// `candidate` against `current` on `channel`. +pub fn compare(channel: &str, candidate: &str, current: &str) -> Result { + match channel { + NIGHTLY => { + let bad = |v: &str| format!("{v:?} is not a nightly version (YEAR.MONTH.DAY.N)"); + Ok(dated(candidate).ok_or_else(|| bad(candidate))?.cmp(&dated(current).ok_or_else(|| bad(current))?)) + } + STABLE => { + let parse = |v: &str| semver::Version::parse(v.trim_start_matches('v')).map_err(|e| format!("{v:?}: {e}")); + Ok(parse(candidate)?.cmp(&parse(current)?)) + } + _ => Err(format!("unknown channel {channel:?}")), + } +} + +/// Whether `candidate` is newer than `current` on `channel`: never a step back, so an old manifest served again is no +/// update. +pub fn newer(channel: &str, candidate: &str, current: &str) -> Result { + compare(channel, candidate, current).map(|o| o == Ordering::Greater) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nightlies_compare_by_date_then_count() { + assert!(newer(NIGHTLY, "2026.11.7.2", "2026.11.7.1").unwrap()); + assert!(newer(NIGHTLY, "2026.11.10.1", "2026.11.7.3").unwrap(), "numbers, not text"); + assert!(newer(NIGHTLY, "2027.1.1.1", "2026.12.31.9").unwrap()); + assert!(!newer(NIGHTLY, "2026.11.7.1", "2026.11.7.1").unwrap()); + assert!(!newer(NIGHTLY, "2026.11.6.4", "2026.11.7.1").unwrap(), "never back"); + } + + #[test] + fn nightly_versions_have_four_parts() { + for bad in ["2026.11.7", "2026.11.7.1.1", "2026.13.1.1", "2026.11.7.0", "0.0.1", "v2026.11.7.1", ""] { + assert!(compare(NIGHTLY, bad, "2026.11.7.1").is_err(), "{bad}"); + } + } + + #[test] + fn stable_versions_are_semantic() { + assert!(newer(STABLE, "1.0.0", "0.9.12").unwrap()); + assert!(newer(STABLE, "1.10.0", "1.9.0").unwrap()); + assert!(newer(STABLE, "v1.0.1", "1.0.0").unwrap(), "a tag's v is allowed"); + assert!(newer(STABLE, "1.0.0", "1.0.0-rc.1").unwrap()); + assert!(newer(STABLE, "0.1.1", "0.1.0").unwrap(), "from alpha to alpha"); + assert!(newer(STABLE, "1.0.0", "0.12.3").unwrap(), "from an alpha on to the first stable release"); + assert!(!newer(STABLE, "0.1.0", "0.1.1").unwrap(), "never back"); + assert!(!newer(STABLE, "1.0.0", "1.0.0").unwrap()); + assert!(compare(STABLE, "2026.11.7.1", "1.0.0").is_err()); + } + + #[test] + fn manifest_urls() { + let base = "https://github.com/ItsAlphaNeon/Lemon-Studio/releases/"; + assert_eq!(url(base, STABLE).unwrap(), "https://github.com/ItsAlphaNeon/Lemon-Studio/releases/latest/download/stable.json"); + assert_eq!(url(base, NIGHTLY).unwrap(), "https://github.com/ItsAlphaNeon/Lemon-Studio/releases/download/nightly/nightly.json"); + assert!(url(base, "beta").is_err()); + } + + /// A manifest signed with a throwaway minisign key (made for this test and thrown away; not the release key). + const KEY: &str = "untrusted comment: minisign public key: F6AB7B4AA4ADAE5B\nRWRbrq2kSnur9jI/ZYtWjdCOEicegzn5/f461vgXZ92Kaeg6J5bhht+y\n"; + const BODY: &[u8] = br#"{"channel":"nightly","version":"2026.11.7.1","assets":{"windows-x86_64":{"url":"https://example.invalid/a.exe","size":3,"sha256":"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"}}}"#; + const SIG: &str = "untrusted comment: signature from rsign secret key +RURbrq2kSnur9oGwtsObmQdSbebup0WmxfV3i9lx6JG9wQtaEK2OqMw6uEw5yL2nvXpL7kVwuPou0MWIWbRVJwCW4ncRARf2XwQ= +trusted comment: lemonstudio nightly 2026.11.7.1 +yBFH76fWe28O6lf7nNetV+sKQgVqbVa/oFOHYguWLovG9++HyRUbDadW7Gcd7mRSNn2pcHhqfKGOM88m+nLeCQ== +"; + + #[test] + fn a_signed_manifest_is_read() { + let m = verified(BODY, SIG, KEY, NIGHTLY).unwrap(); + assert_eq!(m.version, "2026.11.7.1"); + assert_eq!(m.assets["windows-x86_64"].size, 3); + // The key alone (the line `MINISIGN_PUBLIC_KEY` holds) works as well as the whole .pub file. + assert!(verified(BODY, SIG, KEY.lines().nth(1).unwrap(), NIGHTLY).is_ok()); + } + + #[test] + fn a_changed_or_unsigned_manifest_is_refused() { + let changed = String::from_utf8(BODY.to_vec()).unwrap().replace("2026.11.7.1", "2026.11.7.2"); + assert!(verified(changed.as_bytes(), SIG, KEY, NIGHTLY).is_err()); + assert!(verified(BODY, "not a signature", KEY, NIGHTLY).is_err()); + let other_key = "RWQf6LRCGA9i53mlYecO4IzT51TGPpvWucNSCh1CBM0QTaLn73Y7GFO3"; + assert!(verified(BODY, SIG, other_key, NIGHTLY).is_err()); + } + + #[test] + fn a_manifest_for_another_channel_is_refused() { + assert!(verified(BODY, SIG, KEY, STABLE).unwrap_err().contains("nightly channel")); + } +} diff --git a/tools/updater/src/net.rs b/tools/updater/src/net.rs new file mode 100644 index 000000000..641ba8291 --- /dev/null +++ b/tools/updater/src/net.rs @@ -0,0 +1,95 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! HTTPS: rustls (with ring), checked against the system's certificate store. + +use std::io::{Read, Write}; +use std::path::Path; +use std::time::Duration; + +use sha2::{Digest, Sha256}; +use ureq::tls::{Certificate, RootCerts, TlsConfig, TlsProvider}; + +pub struct Net { + agent: ureq::Agent, +} + +impl Net { + pub fn new() -> Result { + // The one crypto provider built in; an error only means one is installed already. + let _ = rustls::crypto::ring::default_provider().install_default(); + let found = rustls_native_certs::load_native_certs(); + let certs: Vec> = found.certs.iter().map(|c| Certificate::from_der(c.as_ref()).to_owned()).collect(); + if certs.is_empty() { + let why = found.errors.first().map(|e| format!(": {e}")).unwrap_or_default(); + return Err(format!("no certificates in the system's certificate store{why}")); + } + let tls = TlsConfig::builder().provider(TlsProvider::Rustls).root_certs(RootCerts::new_with_certs(&certs)).build(); + let config = ureq::Agent::config_builder() + .tls_config(tls) + .user_agent(format!("lemonstudio-updater/{}", env!("CARGO_PKG_VERSION"))) + .timeout_connect(Some(Duration::from_secs(15))) + .timeout_recv_response(Some(Duration::from_secs(30))) + .timeout_per_read(Some(Duration::from_secs(60))) + .build(); + Ok(Net { agent: config.into() }) + } + + /// The body at `url` (redirects followed); None when there is nothing there (404). + pub fn get(&self, url: &str) -> Result>, String> { + match self.agent.get(url).call() { + Ok(mut r) => r.body_mut().with_config().limit(16 << 20).read_to_vec().map(Some).map_err(|e| format!("{url}: {e}")), + Err(ureq::Error::StatusCode(404)) => Ok(None), + Err(e) => Err(format!("{url}: {e}")), + } + } + + /// Downloads `url` to `to`, which must then be `size` bytes with this SHA-256 (lowercase hex); `progress` hears + /// (bytes so far, size) now and then. A file that does not match is deleted. + pub fn download(&self, url: &str, to: &Path, size: u64, sha256: &str, mut progress: impl FnMut(u64, u64)) -> Result<(), String> { + let r = self.agent.get(url).call().map_err(|e| format!("{url}: {e}"))?; + let mut body = r.into_body().into_reader(); + let mut file = std::fs::File::create(to).map_err(|e| format!("{}: {e}", to.display()))?; + let mut hash = Sha256::new(); + let (mut done, mut told) = (0u64, 0u64); + let mut buf = vec![0u8; 1 << 16]; + let result = loop { + let n = match body.read(&mut buf) { + Ok(0) => break Ok(()), + Ok(n) => n, + Err(e) => break Err(format!("{url}: {e}")), + }; + done += n as u64; + if done > size { + break Err(format!("{url}: larger than the manifest says ({size} bytes)")); + } + hash.update(&buf[..n]); + if let Err(e) = file.write_all(&buf[..n]) { + break Err(format!("{}: {e}", to.display())); + } + if done - told >= 1 << 20 { + told = done; + progress(done, size); + } + }; + let result = result + .and_then(|()| file.sync_all().map_err(|e| format!("{}: {e}", to.display()))) + .and_then(|()| if done == size { Ok(()) } else { Err(format!("{url}: {done} bytes, the manifest says {size}")) }) + .and_then(|()| { + let got = hex(&hash.finalize()); + if got.eq_ignore_ascii_case(sha256) { Ok(()) } else { Err(format!("{url}: SHA-256 {got} does not match the manifest's")) } + }); + drop(file); + if result.is_err() { + let _ = std::fs::remove_file(to); + } else { + progress(done, size); + } + result + } +} + +pub fn hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} diff --git a/tools/updater/src/swap.rs b/tools/updater/src/swap.rs new file mode 100644 index 000000000..ab6277c9f --- /dev/null +++ b/tools/updater/src/swap.rs @@ -0,0 +1,257 @@ +// Copyright (C) 2026 ItsAlphaNeon +// SPDX-License-Identifier: GPL-3.0-or-later +// This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio + +//! Putting a downloaded build in place of the installed one, once Lemon Studio has closed, and starting it again. +//! +//! - Windows: the `.exe`. A running program's file cannot be overwritten but can be renamed: the old one goes to +//! `.old` (Lemon Studio deletes it at its next start, should something still hold it now), the new one takes +//! its name. +//! - Linux: the AppImage, replaced at once by a rename over it. +//! - macOS: the `.app` bundle, a folder: the new one is copied next to it with `ditto` (which keeps the signature), the +//! old one moved aside, the new one moved in, the old one deleted. +//! +//! The new copy is written next to the target first, so the last step is a rename within one folder. Should the +//! last step fail, the old program is put back. + +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + +/// Waits up to `timeout` for process `pid` to end; true once it has. +pub fn wait_for_exit(pid: u32, timeout: Duration) -> bool { + #[cfg(windows)] + { + use windows_sys::Win32::Foundation::{CloseHandle, WAIT_OBJECT_0}; + use windows_sys::Win32::System::Threading::{OpenProcess, WaitForSingleObject, PROCESS_SYNCHRONIZE}; + // SAFETY: a handle opened here is waited on and closed here. + unsafe { + let h = OpenProcess(PROCESS_SYNCHRONIZE, 0, pid); + if h.is_null() { + return true; // gone already (or never ours to wait for) + } + let r = WaitForSingleObject(h, timeout.as_millis().min(u32::MAX as u128 - 1) as u32); + CloseHandle(h); + r == WAIT_OBJECT_0 + } + } + #[cfg(unix)] + { + let start = Instant::now(); + loop { + // SAFETY: signal 0 only checks that the process exists. + let gone = unsafe { libc::kill(pid as libc::pid_t, 0) } != 0 + && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH); + if gone || zombie(pid) { + return true; + } + if start.elapsed() >= timeout { + return false; + } + std::thread::sleep(Duration::from_millis(100)); + } + } + #[cfg(not(any(windows, unix)))] + { + let _ = (pid, timeout, Instant::now()); + true + } +} + +/// Linux: a process that has ended but that its parent has not reaped yet still answers signal 0. +#[cfg(unix)] +fn zombie(pid: u32) -> bool { + #[cfg(target_os = "linux")] + { + // `/proc//stat`: "pid (name) state ...": the state follows the last ')'. + std::fs::read_to_string(format!("/proc/{pid}/stat")) + .ok() + .and_then(|s| s.rfind(')').and_then(|i| s[i + 1..].trim_start().chars().next())) + .is_some_and(|c| c == 'Z' || c == 'X') + } + #[cfg(not(target_os = "linux"))] + { + let _ = pid; + false + } +} + +/// Puts `staged` (a file, or a `.app` folder) in place of `target`. +pub fn replace(staged: &Path, target: &Path) -> Result<(), String> { + if staged.is_dir() { replace_bundle(staged, target) } else { replace_file(staged, target) } +} + +fn beside(target: &Path, suffix: &str) -> Result { + let name = target.file_name().ok_or_else(|| format!("{}: no file name", target.display()))?.to_string_lossy(); + let parent = target.parent().ok_or_else(|| format!("{}: no folder", target.display()))?; + Ok(parent.join(format!(".{name}.{suffix}"))) +} + +/// A rename, tried for a few seconds: on Windows a virus scanner or the old process's last handles can hold the file +/// for a moment after it ends. +fn rename(from: &Path, to: &Path) -> Result<(), String> { + let start = Instant::now(); + loop { + match std::fs::rename(from, to) { + Ok(()) => return Ok(()), + Err(e) if start.elapsed() > Duration::from_secs(10) => { + return Err(format!("{} to {}: {e}", from.display(), to.display())); + } + Err(_) => std::thread::sleep(Duration::from_millis(250)), + } + } +} + +fn replace_file(staged: &Path, target: &Path) -> Result<(), String> { + let new = beside(target, "new")?; + let _ = std::fs::remove_file(&new); + std::fs::copy(staged, &new).map_err(|e| format!("{}: {e}", new.display()))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&new, std::fs::Permissions::from_mode(0o755)).map_err(|e| format!("{}: {e}", new.display()))?; + } + #[cfg(windows)] + { + let old = old_exe(target); + let _ = std::fs::remove_file(&old); + if let Err(e) = rename(target, &old) { + let _ = std::fs::remove_file(&new); + return Err(e); + } + if let Err(e) = rename(&new, target) { + let _ = std::fs::rename(&old, target); + let _ = std::fs::remove_file(&new); + return Err(e); + } + let _ = std::fs::remove_file(&old); + Ok(()) + } + #[cfg(not(windows))] + { + rename(&new, target).inspect_err(|_| { + let _ = std::fs::remove_file(&new); + }) + } +} + +/// Windows: where the replaced program goes (Lemon Studio deletes it at its next start). +#[cfg(windows)] +pub fn old_exe(target: &Path) -> PathBuf { + let mut s = target.as_os_str().to_owned(); + s.push(".old"); + PathBuf::from(s) +} + +fn replace_bundle(staged: &Path, target: &Path) -> Result<(), String> { + let (new, old) = (beside(target, "new")?, beside(target, "old")?); + let _ = std::fs::remove_dir_all(&new); + let _ = std::fs::remove_dir_all(&old); + copy_tree(staged, &new)?; + if let Err(e) = rename(target, &old) { + let _ = std::fs::remove_dir_all(&new); + return Err(e); + } + if let Err(e) = rename(&new, target) { + let _ = std::fs::rename(&old, target); + let _ = std::fs::remove_dir_all(&new); + return Err(e); + } + let _ = std::fs::remove_dir_all(&old); + Ok(()) +} + +/// A folder copied with its links, attributes and signature: `ditto` on macOS, `cp -a` elsewhere. +fn copy_tree(from: &Path, to: &Path) -> Result<(), String> { + let mut cmd = if cfg!(target_os = "macos") { + let mut c = Command::new("ditto"); + c.arg(from).arg(to); + c + } else { + let mut c = Command::new("cp"); + c.arg("-a").arg(from).arg(to); + c + }; + match cmd.status() { + Ok(s) if s.success() => Ok(()), + Ok(s) => Err(format!("copying {} failed ({s})", from.display())), + Err(e) => Err(format!("copying {}: {e}", from.display())), + } +} + +/// Starts the program at `target` (the `.exe`, the AppImage or the `.app`), on its own. +pub fn relaunch(target: &Path) -> Result<(), String> { + let mut cmd = if cfg!(target_os = "macos") && target.is_dir() { + let mut c = Command::new("open"); + c.arg(target); + c + } else { + Command::new(target) + }; + if let Some(dir) = target.parent() { + cmd.current_dir(dir); + } + cmd.stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null()); + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + cmd.spawn().map(drop).map_err(|e| format!("starting {}: {e}", target.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn temp(name: &str) -> PathBuf { + let d = std::env::temp_dir().join(format!("lemonstudio-updater-test-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&d); + std::fs::create_dir_all(&d).unwrap(); + d + } + + #[test] + fn a_file_is_replaced_in_place() { + let d = temp("file"); + let (staged, target) = (d.join("staged.bin"), d.join("Lemon Studio.exe")); + std::fs::write(&staged, b"new").unwrap(); + std::fs::write(&target, b"old").unwrap(); + replace(&staged, &target).unwrap(); + assert_eq!(std::fs::read(&target).unwrap(), b"new"); + // Only the program is left (the staged copy is the caller's to delete). + let mut left: Vec = std::fs::read_dir(&d).unwrap().map(|e| e.unwrap().file_name().to_string_lossy().into_owned()).collect(); + left.sort(); + assert_eq!(left, ["Lemon Studio.exe", "staged.bin"]); + std::fs::remove_dir_all(&d).unwrap(); + } + + #[cfg(unix)] + #[test] + fn a_bundle_is_replaced_in_place() { + let d = temp("bundle"); + let (staged, target) = (d.join("staged/Lemon Studio.app"), d.join("Lemon Studio.app")); + std::fs::create_dir_all(staged.join("Contents/MacOS")).unwrap(); + std::fs::write(staged.join("Contents/MacOS/lemonstudio"), b"new").unwrap(); + std::fs::create_dir_all(target.join("Contents/MacOS")).unwrap(); + std::fs::write(target.join("Contents/MacOS/lemonstudio"), b"old").unwrap(); + std::fs::write(target.join("Contents/stale"), b"gone after").unwrap(); + replace(&staged, &target).unwrap(); + assert_eq!(std::fs::read(target.join("Contents/MacOS/lemonstudio")).unwrap(), b"new"); + assert!(!target.join("Contents/stale").exists()); + assert!(!d.join(".Lemon Studio.app.old").exists()); + std::fs::remove_dir_all(&d).unwrap(); + } + + #[test] + fn an_ended_process_is_not_waited_for() { + let mut child = if cfg!(windows) { + Command::new("cmd").args(["/C", "exit 0"]).spawn().unwrap() + } else { + Command::new("true").spawn().unwrap() + }; + let pid = child.id(); + child.wait().unwrap(); + assert!(wait_for_exit(pid, Duration::from_secs(5))); + } +}