From 54eaf6027c5e56a28a1b27cb61eb6a2ddf3da8c8 Mon Sep 17 00:00:00 2001 From: AlphaNeon <102443376+ItsAlphaNeon@users.noreply.github.com> Date: Sat, 7 Nov 2026 16:48:14 -0700 Subject: [PATCH 1/5] Updater: release builds check their channel and update themselves Nightly and stable builds made by the new release workflow carry lemonstudio-updater (tools/updater), which checks the channel's signed manifest, downloads the newer build and, once Lemon Studio closes, replaces the .exe, the AppImage or the .app and starts it again. Builds from source have no channel and never update. - Versions: nightlies are dated (YEAR.MONTH.DAY.N), stable releases semantic; the app, diagnostics, panic log and the Windows version resource show the build's version (version.rs). - In the app (update.rs): the check at start, the Install and restart / Skip this version / Later prompt, HELP > Check for updates, the General > Updates checks, and the About tab's channel line and Check for updates button. - Release workflow: a nightly from main every night (if main moved), a stable release from a vMAJOR.MINOR.PATCH tag; Windows .exe, Linux AppImage, macOS .app (Apple silicon, not notarized: the release notes say how to open it) and the VST3/CLAP plugins as a zip per platform. Manifests are signed with minisign (MINISIGN_SECRET_KEY secret, MINISIGN_PUBLIC_KEY variable). - docs/releasing.md, README, alpha checklist, third-party notices. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 288 +++++ Cargo.lock | 226 ++++ Cargo.toml | 2 +- README.md | 10 +- THIRD-PARTY-NOTICES.txt | 1614 ++++++++++++++++++++++----- crates/os-app/build.rs | 76 +- crates/os-app/src/app.rs | 20 +- crates/os-app/src/control/rpc.rs | 2 +- crates/os-app/src/diagnostics.rs | 4 +- crates/os-app/src/dialog.rs | 2 + crates/os-app/src/main.rs | 6 + crates/os-app/src/panic_log.rs | 2 +- crates/os-app/src/prefs.rs | 35 +- crates/os-app/src/update.rs | 558 +++++++++ crates/os-app/src/version.rs | 38 + crates/os-app/src/views/menus.rs | 5 +- crates/os-app/src/views/mod.rs | 6 +- crates/os-app/src/views/settings.rs | 27 +- docs/alpha-checklist.md | 17 +- docs/releasing.md | 73 ++ tools/bundle-app/src/main.rs | 15 +- tools/package/appimage.sh | 50 + tools/package/manifest.py | 87 ++ tools/package/plugins-readme.txt | 26 + tools/package/release-notes.md | 27 + tools/updater/Cargo.toml | 28 + tools/updater/src/main.rs | 202 ++++ tools/updater/src/manifest.rs | 181 +++ tools/updater/src/net.rs | 95 ++ tools/updater/src/swap.rs | 257 +++++ 30 files changed, 3671 insertions(+), 308 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 crates/os-app/src/update.rs create mode 100644 crates/os-app/src/version.rs create mode 100644 docs/releasing.md create mode 100755 tools/package/appimage.sh create mode 100644 tools/package/manifest.py create mode 100644 tools/package/plugins-readme.txt create mode 100644 tools/package/release-notes.md create mode 100644 tools/updater/Cargo.toml create mode 100644 tools/updater/src/main.rs create mode 100644 tools/updater/src/manifest.rs create mode 100644 tools/updater/src/net.rs create mode 100644 tools/updater/src/swap.rs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..cbad25942 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,288 @@ +# 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. +# - Stable: a pushed tag vMAJOR.MINOR.PATCH, which must match the workspace's version in Cargo.toml. Its release holds +# stable.json; GitHub's "latest" release is the newest stable one. +# +# 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 + +permissions: + contents: write + +concurrency: + group: release + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + KEEP_NIGHTLIES: 14 + +jobs: + plan: + name: Plan + 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 }} + run: | + set -euo pipefail + sha=$(git rev-parse HEAD) + skip=false + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + channel=stable + tag=$GITHUB_REF_NAME + version=${tag#v} + workspace=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml) + if [ "$version" != "$workspace" ]; then + echo "::error::The tag $tag does not match the workspace version $workspace in Cargo.toml" + exit 1 + fi + previous=$( (git tag -l 'v*.*.*' --sort=-v:refname | grep -vx "$tag" || true) | head -n1) + else + if [ "$GITHUB_REF" != refs/heads/main ]; then + echo "::error::Nightlies are built from main (this run is on $GITHUB_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 ]; then + echo "main has not changed since $previous: no nightly" + skip=true + fi + fi + echo "$channel $version ($tag at $sha), after ${previous:-nothing}" + { + echo "channel=$channel" + echo "version=$version" + echo "tag=$tag" + echo "previous=$previous" + echo "sha=$sha" + echo "skip=$skip" + } >>"$GITHUB_OUTPUT" + + 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 + mkdir -p dist "plugins/LemonStudio-Plugins-$v/VST3" "plugins/LemonStudio-Plugins-$v/CLAP" + case "$RUNNER_OS" in + Windows) + cp target/release/lemonstudio.exe "dist/LemonStudio-$v-$PLATFORM.exe" + ;; + Linux) + curl -fsSL -o "$RUNNER_TEMP/appimagetool" \ + https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage + chmod +x "$RUNNER_TEMP/appimagetool" + APPIMAGE_EXTRACT_AND_RUN=1 APPIMAGETOOL="$RUNNER_TEMP/appimagetool" \ + bash tools/package/appimage.sh target/release/lemonstudio "$v" "dist/LemonStudio-$v-$PLATFORM.AppImage" + ;; + macOS) + ditto -c -k --keepParent "target/bundled/Lemon Studio.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 + args=(-f "tag_name=$TAG" -f "target_commitish=$SHA") + [ -n "$PREVIOUS" ] && args+=(-f "previous_tag_name=$PREVIOUS") + gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" "${args[@]}" --jq .body >"$RUNNER_TEMP/changes.md" + if [ "$CHANNEL" = nightly ]; then + intro="A nightly build of main at ${SHA:0:8}: the newest changes, and the least tested. Lemon Studio is in its pre-alpha." + else + intro="Lemon Studio $VERSION." + fi + 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() + print(text.replace("{INTRO}", sys.argv[2]).replace("{VERSION}", os.environ["VERSION"]).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: Publish the release + run: | + set -euo pipefail + if [ "$CHANNEL" = stable ]; then + gh release create "$TAG" dist/* --verify-tag --title "Lemon Studio $VERSION" --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' + 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 f9879ae83..8f591b18d 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" @@ -2331,6 +2373,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" @@ -2931,6 +2979,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" @@ -3613,6 +3667,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" @@ -3666,6 +3734,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" @@ -3681,6 +3796,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" @@ -3706,6 +3830,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" @@ -3772,6 +3919,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" @@ -3946,6 +4104,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" @@ -4453,6 +4617,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" @@ -4472,6 +4686,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" @@ -5349,6 +5569,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 d4d8386b6..c8b5c3c50 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"] +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"] [workspace.package] version = "0.0.1" diff --git a/README.md b/README.md index 5245c96b0..9e6c26138 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,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 +**Nightly builds** for Windows, Linux (AppImage) and macOS (Apple silicon) are on the +[releases page](https://github.com/ItsAlphaNeon/Lemon-Studio/releases): the newest changes on main, built every night, +with Lemon Studio's plugins for other DAWs as a separate zip. They are pre-alpha and not signed: the release notes say +how to open them on each system (on macOS, past Gatekeeper). Once installed, a nightly checks for a newer nightly when +it starts and installs it when you agree (Settings ▸ General ▸ Updates); stable releases will do the same with stable +releases. 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: @@ -356,6 +363,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/build.rs b/crates/os-app/build.rs index cdb125eb9..fc3ab3804 100644 --- a/crates/os-app/build.rs +++ b/crates/os-app/build.rs @@ -2,9 +2,10 @@ // 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` 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, and `cfg(lemon_updater)` when +//! `LEMON_UPDATER` names the updater to carry (release builds, `crate::update`). use std::path::PathBuf; use std::process::Command; @@ -57,8 +58,69 @@ 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()) +} + +/// 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). +fn resource_script(ico: &std::path::Path, version: &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", "Lemon Studio" + 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", "Lemon Studio" + VALUE "ProductVersion", "{text}" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END +"# + ) +} + fn main() { factory_presets(); + updater(); + let version = version(); // `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)"); @@ -76,11 +138,15 @@ 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 = std::path::absolute("assets/lemonstudio.ico").expect("icon path"); + let script = out_dir.join("lemonstudio.rc"); + std::fs::write(&script, resource_script(&ico, &version)).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 2201c43cf..430df350a 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()) } @@ -16130,7 +16144,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..e71e3938b 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 {} (pre-alpha)", crate::version::text()), 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 {} (pre-alpha)\n", crate::version::text())), "{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/main.rs b/crates/os-app/src/main.rs index adf14573d..d93020e9c 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; @@ -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); } @@ -1979,6 +1983,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/update.rs b/crates/os-app/src/update.rs new file mode 100644 index 000000000..7633a505a --- /dev/null +++ b/crates/os-app/src/update.rs @@ -0,0 +1,558 @@ +// 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. +fn folder() -> Option { + os_paths::user_file("Updates") +} + +/// 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("This copy of Lemon Studio is not inside its Lemon Studio.app, so it can't update itself.".into()); + }; + // 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("Move Lemon Studio.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.".into()); + } + 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 = CHANNEL.unwrap_or(""); + 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..9252ad777 --- /dev/null +++ b/crates/os-app/src/version.rs @@ -0,0 +1,38 @@ +// 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 stable +//! release's is semantic, `MAJOR.MINOR.PATCH` (the workspace's `version`). 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"); + +/// 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(), + }) +} + +#[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")); + } + } +} 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..bab668bd8 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{} - pre-alpha", crate::version::text()); 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 91fd2392b..a84d58a4f 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.", + (Some(_), true) => "It updates itself from the stable releases.", + _ => "Built from its source code: it doesn't update itself (git pull, then build it again).", + }; + 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::text())), 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 590acd5ea..f47c13ac4 100644 --- a/docs/alpha-checklist.md +++ b/docs/alpha-checklist.md @@ -209,14 +209,19 @@ 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. - -- [ ] **Prebuilt binaries for Windows, macOS and Linux.** Today Lemon Studio is built from source only +What has to exist before anyone outside can run the alpha. Stable releases ship once the rest of this checklist is +done (decided 2026-10-29), so these come last. 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.** The release workflow builds them, a nightly from main every + night and a stable 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: its first run, and trying the updates on each platform - [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..f0437fa13 --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,73 @@ +# 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`, 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 | +| Stable (after the alpha) | Pushing a tag `vMAJOR.MINOR.PATCH` | Semantic: the workspace's `version` in `Cargo.toml`, which the tag must match | Release `v1.0.0`, holding `stable.json`; GitHub's *latest* release | + +Each release has: + +- `LemonStudio--windows-x86_64.exe` +- `LemonStudio--linux-x86_64.AppImage`, built on Ubuntu 22.04 by [tools/package/appimage.sh](../tools/package/appimage.sh) +- `LemonStudio--macos-aarch64.zip`, holding `Lemon Studio.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 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. + +## 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` +- stable: `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. + +## The signing key (once) + +Manifests are signed with [minisign](https://jedisct1.github.io/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. + +## Making a stable release + +1. Set `version` under `[workspace.package]` in `Cargo.toml`, e.g. `1.0.0`, and merge it. +2. Tag the merge commit and push the tag: `git tag v1.0.0 && git push origin v1.0.0`. diff --git a/tools/bundle-app/src/main.rs b/tools/bundle-app/src/main.rs index 331fe0b53..c333463d7 100644 --- a/tools/bundle-app/src/main.rs +++ b/tools/bundle-app/src/main.rs @@ -11,6 +11,9 @@ //! 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. +//! //! The macOS counterpart of Linux's desktop entry (`crates/os-app/assets/lemonstudio.desktop`). use std::path::PathBuf; @@ -72,8 +75,16 @@ fn run(cmd: &mut Command, what: &str) { assert!(cmd.status().unwrap_or_else(|e| panic!("{what}: {e}")).success(), "{what} failed"); } +/// 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() -> String { - let version = env!("CARGO_PKG_VERSION"); + let (short, version) = versions(); format!( r#" @@ -94,7 +105,7 @@ fn info_plist() -> String { CFBundlePackageType APPL CFBundleShortVersionString - {version} + {short} CFBundleVersion {version} CFBundleInfoDictionaryVersion diff --git a/tools/package/appimage.sh b/tools/package/appimage.sh new file mode 100755 index 000000000..27156fa5b --- /dev/null +++ b/tools/package/appimage.sh @@ -0,0 +1,50 @@ +#!/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 +# +# The AppDir holds the program, its desktop entry (crates/os-app/assets/lemonstudio.desktop, with the version added) +# and its icon. 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 [ $# -ne 3 ]; then + echo "usage: $0 " >&2 + exit 2 +fi +bin=$1 +version=$2 +out=$3 +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/lemonstudio.png" "$appdir/usr/share/icons/hicolor/256x256/apps/lemonstudio.png" +install -m644 "$assets/lemonstudio.png" "$appdir/lemonstudio.png" +ln -s lemonstudio.png "$appdir/.DirIcon" +sed "/^\[Desktop Entry\]/a X-AppImage-Version=$version" "$assets/lemonstudio.desktop" >"$appdir/lemonstudio.desktop" +cp "$appdir/lemonstudio.desktop" "$appdir/usr/share/applications/lemonstudio.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..4768db5d9 --- /dev/null +++ b/tools/package/manifest.py @@ -0,0 +1,87 @@ +#!/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: `LemonStudio--.`, `` as +Rust names it (`windows-x86_64`, `linux-x86_64`, `macos-aarch64`). 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 assets(dist, version, base): + pattern = re.compile(r"^LemonStudio-" + re.escape(version) + r"-(?P[a-z0-9]+-[a-z0-9_]+)\.(exe|AppImage|zip)$") + found = {} + for name in sorted(os.listdir(dist)): + m = pattern.match(name) + if not m: + continue + path = os.path.join(dist, name) + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(1 << 20), b""): + h.update(chunk) + found[m.group("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.version, f"https://github.com/{a.repo}/releases/download/{a.tag}") + if not found: + sys.exit(f"no LemonStudio-{a.version}- programs in {a.dist}") + 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/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..7bf084122 --- /dev/null +++ b/tools/package/release-notes.md @@ -0,0 +1,27 @@ +{INTRO} + +### Downloads + +| | Lemon Studio | Its plugins for other DAWs (VST3 and CLAP) | +|---|---|---| +| Windows | `LemonStudio-{VERSION}-windows-x86_64.exe` | `LemonStudio-Plugins-{VERSION}-windows-x86_64.zip` | +| Linux | `LemonStudio-{VERSION}-linux-x86_64.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. + +- **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 LemonStudio-*.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 **Lemon Studio.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/Lemon Studio.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/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..675110492 --- /dev/null +++ b/tools/updater/src/manifest.rs @@ -0,0 +1,181 @@ +// 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 +//! stable release 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, "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))); + } +} From f513a791266a68dbe3ee0859832928b91a7db216 Mon Sep 17 00:00:00 2001 From: AlphaNeon <102443376+ItsAlphaNeon@users.noreply.github.com> Date: Sun, 8 Nov 2026 14:45:41 -0700 Subject: [PATCH 2/5] HELP menu: opens the website, and About works Help index, Tutorials and the new Keyboard shortcuts, Troubleshooting and Lemon Studio website entries open the website's wiki instead of the README. Report a bug is new and opens GitHub's bug report form. What's new opens the GitHub releases, where the website's Changelog link goes too. About... only wrote a line to the hint bar. It now opens the About page of the settings, as do the toolbar's logo, a right click on its Help button and the macOS application menu. That page gains Website, Wiki and Source code buttons, and its version line says pre-alpha, as the old message did. A left click on the toolbar's Help button did nothing and now opens the wiki. A mixer slot's Help on a built-in plugin or a container opens its own wiki page. Tutorials no longer lists Scripting and assistants, as on label-sweep. Spec: ui/menus.md, ui/toolbar-buttons.md, ui/plugin-window.md, ui/settings.md. Co-Authored-By: Claude Opus 5.5 --- crates/os-app/src/app.rs | 34 +++--- crates/os-app/src/control/tests.rs | 2 +- crates/os-app/src/help_links.rs | 154 +++++++++++++++++++++++----- crates/os-app/src/mixer.rs | 21 ++-- crates/os-app/src/playlist.rs | 3 +- crates/os-app/src/views/menus.rs | 14 ++- crates/os-app/src/views/settings.rs | 11 +- docs/status.md | 11 +- 8 files changed, 184 insertions(+), 66 deletions(-) diff --git a/crates/os-app/src/app.rs b/crates/os-app/src/app.rs index 219dda1a1..dcd97d3a5 100644 --- a/crates/os-app/src/app.rs +++ b/crates/os-app/src/app.rs @@ -5614,7 +5614,7 @@ impl App { self.state.message = Some(format!("Automatic crossfades: {}", if on { "on" } else { "off" })); } - fn settings_act(&mut self, a: views::settings::SAct) { + pub(crate) fn settings_act(&mut self, a: views::settings::SAct) { use views::settings::SAct; // A click anywhere but the colour picker and the colour circles closes the picker. if !matches!(a, SAct::Swatch(..) | SAct::PickBody | SAct::PickPart(_) | SAct::PickSv | SAct::PickHue | SAct::PickRevert) { @@ -5769,6 +5769,9 @@ 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(views::settings::WEBSITE) => self.open_web(crate::help_links::WEBSITE_URL), + SAct::Button(views::settings::WIKI) => self.open_web(crate::help_links::HELP_INDEX_URL), + SAct::Button(views::settings::SOURCE_CODE) => self.open_web(crate::help_links::SOURCE_CODE_URL), 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), @@ -9131,8 +9134,7 @@ impl App { piano::center_keys(s, self.piano_grid.map_or(700.0, |g| g.h)); } } - // No help index yet: the left click does nothing. - Action::Util(Util::Help) => {} + Action::Util(Util::Help) => self.open_web(crate::help_links::HELP_INDEX_URL), Action::Util(Util::Save) => self.menu_item(0, views::menus::FILE_SAVE_AS), Action::Util(Util::AudioEditor) => self.audio_editor(0, false), Action::Util(Util::NewAudioEditor) => self.audio_editor(0, true), @@ -9518,8 +9520,7 @@ impl App { pub(crate) fn menu_item(&mut self, menu: usize, item: usize) { use views::menus::*; - // FILE's first entries and About go by their place; every other entry by its command (`MenuCmd`), never its - // label. + // FILE's first entries go by their place; every other entry by its command (`MenuCmd`), never its label. match (menu, item) { (0, FILE_NEW) => return self.ask_discard("starting a new project", crate::dialog::Next::NewProject), (0, FILE_OPEN) => return self.ask_discard("opening another project", crate::dialog::Next::OpenDialog), @@ -9533,12 +9534,6 @@ impl App { self.want_close = true; return; } - (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}", crate::version::text())); - return; - } (PATTERNS, i) if i >= PATTERN_PICK => return crate::patterns::select(&mut self.state, (i - PATTERN_PICK) as u16), (0, i) if i >= RECENT_PICK => return self.open_recent(i - RECENT_PICK), _ => {} @@ -9572,10 +9567,7 @@ impl App { MenuCmd::Rescan => self.start_scan(crate::plugin_db::Mode::Changed), // The plugin menus' display mode, as their own radios set it (spec: ui/plugin-menu.md §2-3). MenuCmd::View(v) => self.state.message = crate::plugin_menu::run(self, crate::plugin_menu::PlugCmd::View(v)), - MenuCmd::Web(url) => { - crate::help_links::open(url); - self.state.message = Some(format!("Opening {url}")); - } + MenuCmd::Web(url) => self.open_web(url), MenuCmd::SaveMidiClip => self.save_midi_clipboard(), MenuCmd::SaveAudioClip => self.save_audio_clipboard(), MenuCmd::Diagnostics => self.save_diagnostics(), @@ -21888,15 +21880,17 @@ mod tests { assert_eq!(hint(&app, &Action::Util(Util::AudioEditor)), "Open audio editor / New audio editor"); // Hidden until it has a shop icon of its own. assert!(!app.hits.iter().any(|h| h.action == Action::Placeholder("Plugin repository"))); - // Help: the left click does nothing; the right click is About, on the release over the button only. + // Help: the left click opens the help index; the right click is About, on the release over the button only. let help = hit_rect(&app, &Action::Util(Util::Help)); - app.state.message = None; click(&mut app, &mut c, help); - assert!(app.state.message.is_none() && app.state.dialogs.is_empty()); + assert_eq!(app.state.message, Some(format!("Opening {}", crate::help_links::HELP_INDEX_URL))); + assert!(app.state.dialogs.is_empty()); right(&mut app, &mut c, help, (help.cx() + 40.0, help.cy())); - assert!(app.state.message.is_none()); + assert!(!app.state.settings.open); right(&mut app, &mut c, help, (help.cx(), help.cy())); - assert!(app.state.message.as_deref().is_some_and(|m| m.starts_with("Lemon Studio ")), "{:?}", app.state.message); + assert_eq!((app.state.settings.open, app.state.settings.tab), (true, views::settings::ABOUT)); + app.state.settings.open = false; + frame(&mut app, &mut c, None); // Undo: EDIT > Undo; its right click does nothing yet. let undo = hit_rect(&app, &Action::Util(Util::Undo)); app.state.message = None; diff --git a/crates/os-app/src/control/tests.rs b/crates/os-app/src/control/tests.rs index 73c079f1f..b37ace422 100644 --- a/crates/os-app/src/control/tests.rs +++ b/crates/os-app/src/control/tests.rs @@ -1032,7 +1032,7 @@ fn a_hosted_slots_scaling_and_help() { assert_eq!((a.plugins.scale_for(fx, 1.0), scaling(&a).children[2].mark.clone()), (1.25, Mark::Radio(true))); run(&mut a, MCmd::SlotScale(0, None)); assert_eq!(a.plugins.scale_for(fx, 1.5), 1.5, "Main"); - assert_eq!(run(&mut a, MCmd::SlotHelp(0)), Some(format!("Opening {}", crate::help_links::BUILTIN_PLUGINS_URL))); + assert_eq!(run(&mut a, MCmd::SlotHelp(0)), Some(format!("Opening {}", crate::help_links::plugin_page("Lemon Sampler")))); // No instance (an effect the engine plays itself has no editor): greyed. a.plugins.hosted.clear(); assert!(!scaling(&a).enabled); diff --git a/crates/os-app/src/help_links.rs b/crates/os-app/src/help_links.rs index e6c3041d6..7f6ff53a7 100644 --- a/crates/os-app/src/help_links.rs +++ b/crates/os-app/src/help_links.rs @@ -2,26 +2,73 @@ // SPDX-License-Identifier: GPL-3.0-or-later // This file is part of Lemon Studio: https://github.com/ItsAlphaNeon/Lemon-Studio -//! HELP ▸ Help index, Tutorials ▸ and What's new (Lemon Studio's design): Lemon Studio has no help files or tutorials -//! of its own yet, so they open the project's pages in the default web browser: its README (what works and how to -//! start), the README's how-to sections one by one, and its merged pull requests (the changes, newest first). -//! Diagnostics writes a report (`crate::diagnostics`). - -pub const HELP_INDEX_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio#readme"; -pub const WHATS_NEW_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio/pulls?q=is%3Apr+is%3Amerged"; - -/// A plugin menu's Help for Lemon Studio's own effects and instruments (and a plugin that names no page of its own is -/// told apart from them): the feature status page's section on them. -pub const BUILTIN_PLUGINS_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio/blob/main/docs/status.md#-built-in-instruments-and-effects"; - -/// HELP ▸ Tutorials ▸: the README's how-to sections, as (entry, page). -pub const TUTORIALS: [(&str, &str); 4] = [ - ("Getting started", "https://github.com/ItsAlphaNeon/Lemon-Studio#getting-started"), - ("Using Lemon Studio", "https://github.com/ItsAlphaNeon/Lemon-Studio#using-lemon-studio"), - ("Scripting and assistants", "https://github.com/ItsAlphaNeon/Lemon-Studio#scripting-and-assistants"), - ("Command-line tools", "https://github.com/ItsAlphaNeon/Lemon-Studio#command-line-tools"), +//! The pages Lemon Studio opens in the default web browser (Lemon Studio's design): HELP's entries, the toolbar's Help +//! button, the About page's buttons and a plugin's Help. They go to the website's wiki, with what the website leaves +//! out on GitHub: the releases (the site has no changelog of its own and links to them too), bug reports and the +//! source code. Diagnostics writes a report instead (`crate::diagnostics`). + +pub const WEBSITE_URL: &str = "https://getlemonstudio.app/"; +pub const HELP_INDEX_URL: &str = "https://getlemonstudio.app/wiki/"; +pub const SHORTCUTS_URL: &str = "https://getlemonstudio.app/wiki/reference/keyboard-shortcuts/"; +pub const TROUBLESHOOTING_URL: &str = "https://getlemonstudio.app/wiki/reference/troubleshooting/"; +pub const WHATS_NEW_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio/releases"; +pub const REPORT_BUG_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio/issues/new/choose"; +pub const SOURCE_CODE_URL: &str = "https://github.com/ItsAlphaNeon/Lemon-Studio"; + +/// HELP ▸ Tutorials ▸, as (entry, page): the wiki's pages for starting out, in the order it gives them. +pub const TUTORIALS: [(&str, &str); 5] = [ + ("Start here", "https://getlemonstudio.app/wiki/getting-started/"), + ("Set up your audio", "https://getlemonstudio.app/wiki/getting-started/audio-setup/"), + ("A tour of the window", "https://getlemonstudio.app/wiki/getting-started/tour/"), + ("Your first song", "https://getlemonstudio.app/wiki/getting-started/first-song/"), + ("Command-line rendering", "https://getlemonstudio.app/wiki/reference/command-line-rendering/"), +]; + +/// The wiki's overview of Lemon Studio's own plugins. +pub const PLUGINS_URL: &str = "https://getlemonstudio.app/wiki/plugins/"; + +/// The wiki's page on each of Lemon Studio's own plugins that can sit in a mixer slot, as (plugin, page under +/// `PLUGINS_URL`). +const PLUGIN_PAGES: [(&str, &str); 28] = [ + ("Lemon Sampler", "instruments/lemon-sampler"), + ("Lemon Trio", "instruments/lemon-trio"), + ("Citrus", "instruments/citrus"), + ("Smith", "instruments/smith"), + ("Lemon Slicer", "instruments/lemon-slicer"), + ("Lemon Drum Sampler", "instruments/lemon-drum-sampler"), + ("Lemon Parametric EQ", "effects/lemon-parametric-eq"), + ("Pulp Filter", "effects/pulp-filter"), + ("Press", "effects/press"), + ("Lemon Limiter", "effects/lemon-limiter"), + ("Lemon Reverb", "effects/lemon-reverb"), + ("Lemon Delay", "effects/lemon-delay"), + ("Lemon Soft Clipper", "effects/lemon-soft-clipper"), + ("Lemon Fast Distortion", "effects/lemon-fast-distortion"), + ("Lemon Balance", "effects/lemon-balance"), + ("Lemon Stereo Enhancer", "effects/lemon-stereo-enhancer"), + ("Lemon Transient Processor", "effects/lemon-transient-processor"), + ("Lemon Chorus", "effects/lemon-chorus"), + ("Lemon Stereo Shaper", "effects/lemon-stereo-shaper"), + ("Lemon Filter", "effects/lemon-filter"), + ("Lemon Flanger", "effects/lemon-flanger"), + ("Lemon Phaser", "effects/lemon-phaser"), + ("Lemon Vocoder", "effects/lemon-vocoder"), + ("Lemon Drop", "effects/lemon-drop"), + ("Lemon Peak Controller", "controllers/lemon-peak-controller"), + ("Lemon Formula Controller", "controllers/lemon-formula-controller"), + ("Tesla", "tools/tesla"), + ("Lemon Container", "tools/lemon-container"), ]; +/// A plugin menu's Help for Lemon Studio's own plugin `name`: its page in the wiki, or the wiki's overview of them if +/// it has none. +pub fn plugin_page(name: &str) -> String { + match PLUGIN_PAGES.iter().find(|&&(plugin, _)| plugin == name) { + Some((_, page)) => format!("{PLUGINS_URL}{page}/"), + None => PLUGINS_URL.to_owned(), + } +} + /// HELP ▸ Tutorials ▸'s entries. pub fn tutorial_items() -> Vec { TUTORIALS.iter().map(|&(label, url)| crate::playlist::PItem::new(label, crate::playlist::Cmd::Web(url))).collect() @@ -40,33 +87,90 @@ pub fn open(url: &str) { let _ = std::process::Command::new("xdg-open").arg(url).spawn(); } +impl crate::app::App { + /// Opens `url` in the default web browser, the hint bar saying so. + pub(crate) fn open_web(&mut self, url: &str) { + open(url); + self.state.message = Some(format!("Opening {url}")); + } +} + #[cfg(test)] mod tests { - use crate::views::menus::{item_at, works, HELP_MENU}; + use crate::views::menus::{item_at, works, HELP_ABOUT, HELP_MENU}; - /// Help index and What's new work and open their pages. + /// HELP's entries that go online work and open their pages. #[test] - fn help_index_and_whats_new_open_the_project_pages() { + fn help_entries_open_their_pages() { let mut app = crate::app::App::new(None); let at = |label: &str| (0..20).find(|&i| item_at(HELP_MENU, i).is_some_and(|it| it.label == label)).unwrap(); - for (label, url) in [("Help index", super::HELP_INDEX_URL), ("What's new", super::WHATS_NEW_URL)] { + for (label, url) in [ + ("Help index", super::HELP_INDEX_URL), + ("Keyboard shortcuts", super::SHORTCUTS_URL), + ("Troubleshooting", super::TROUBLESHOOTING_URL), + ("Report a bug", super::REPORT_BUG_URL), + ("What's new", super::WHATS_NEW_URL), + ("Lemon Studio website", super::WEBSITE_URL), + ] { assert!(works(HELP_MENU, item_at(HELP_MENU, at(label)).unwrap())); app.menu_item(HELP_MENU, at(label)); assert_eq!(app.state.message.as_deref(), Some(format!("Opening {url}").as_str())); } } - /// Tutorials ▸ works: its submenu lists the README's how-to sections, each opening its page. + /// Help goes to the website: the wiki is what Help index, the tutorials and a plugin's Help open. + #[test] + fn help_goes_to_the_website() { + let pages = [super::HELP_INDEX_URL, super::SHORTCUTS_URL, super::TROUBLESHOOTING_URL, super::PLUGINS_URL]; + for url in pages.into_iter().chain(super::TUTORIALS.map(|(_, url)| url)) { + assert!(url.starts_with("https://getlemonstudio.app/wiki/"), "{url}"); + } + } + + /// Tutorials ▸ works: its submenu lists the wiki's pages for starting out, each opening its page. #[test] - fn tutorials_open_the_readme_sections() { + fn tutorials_open_their_pages() { use crate::views::menus::submenu; let mut app = crate::app::App::new(None); let at = (0..20).find(|&i| item_at(HELP_MENU, i).is_some_and(|it| it.label == "Tutorials")).unwrap(); assert!(works(HELP_MENU, item_at(HELP_MENU, at).unwrap())); let items = submenu(HELP_MENU, "Tutorials").unwrap()(&app.state); let labels: Vec<&str> = items.iter().map(|i| i.label.as_ref()).collect(); - assert_eq!(labels, ["Getting started", "Using Lemon Studio", "Scripting and assistants", "Command-line tools"]); + assert_eq!(labels, ["Start here", "Set up your audio", "A tour of the window", "Your first song", "Command-line rendering"]); crate::playlist::run(&mut app, items[0].cmd.clone()); assert_eq!(app.state.message.as_deref(), Some(format!("Opening {}", super::TUTORIALS[0].1).as_str())); } + + /// About... opens the settings window on its About page, however it's reached. + #[test] + fn about_opens_the_about_page() { + use crate::views::settings::{ABOUT, TABS}; + let mut app = crate::app::App::new(None); + assert_eq!(TABS[ABOUT].0, "About"); + assert!(works(HELP_MENU, item_at(HELP_MENU, HELP_ABOUT).unwrap())); + app.menu_item(HELP_MENU, HELP_ABOUT); + assert!(app.state.settings.open); + assert_eq!(app.state.settings.tab, ABOUT); + } + + /// The About page's buttons open the project's pages. + #[test] + fn about_page_buttons_open_the_project_pages() { + use crate::views::settings::{SAct, SOURCE_CODE, WEBSITE, WIKI}; + let mut app = crate::app::App::new(None); + for (label, url) in [(WEBSITE, super::WEBSITE_URL), (WIKI, super::HELP_INDEX_URL), (SOURCE_CODE, super::SOURCE_CODE_URL)] { + app.settings_act(SAct::Button(label)); + assert_eq!(app.state.message.as_deref(), Some(format!("Opening {url}").as_str())); + } + } + + /// Every plugin built into Lemon Studio has its page in the wiki; any other name gets the overview. + #[test] + fn built_in_plugins_have_their_pages() { + for a in crate::plugins::builtin().iter().chain(&crate::plugins::builtin_effects()) { + assert_ne!(super::plugin_page(&a.desc.name), super::PLUGINS_URL, "{}", a.desc.name); + } + assert_eq!(super::plugin_page(crate::container::CONTAINER), "https://getlemonstudio.app/wiki/plugins/tools/lemon-container/"); + assert_eq!(super::plugin_page("Not a plugin"), super::PLUGINS_URL); + } } diff --git a/crates/os-app/src/mixer.rs b/crates/os-app/src/mixer.rs index e230f1055..3bc8ab325 100644 --- a/crates/os-app/src/mixer.rs +++ b/crates/os-app/src/mixer.rs @@ -544,9 +544,9 @@ fn changed(s: &mut State) { } /// The help page of the plugin in slot `slot` of the current track (spec: ui/plugin-window.md "Help", Lemon Studio's -/// design): a hosted plugin's own page (`os_host::Plugin::help_url`); for Lemon Studio's own effects, and the effects -/// the engine plays itself, the feature status page's section on them. `None`: an empty slot, or a hosted plugin that -/// names no page. +/// design): a hosted plugin's own page (`os_host::Plugin::help_url`); for Lemon Studio's own plugins, the effects the +/// engine plays itself and a container, its page in the wiki. `None`: an empty slot, or a hosted plugin that names no +/// page. pub(crate) fn slot_help(app: &App, slot: u8) -> Option { let s = &app.state; let t = s.selected_mixer; @@ -555,9 +555,12 @@ pub(crate) fn slot_help(app: &App, slot: u8) -> Option { Some(h) => { let p = h.plugin.lock().ok()?; let own = crate::plugins::builtin_effects().iter().chain(&crate::plugins::builtin()).any(|a| a.desc.id == p.desc().id); - p.help_url().or_else(|| own.then(|| crate::help_links::BUILTIN_PLUGINS_URL.to_owned())) + p.help_url().or_else(|| own.then(|| crate::help_links::plugin_page(&p.desc().name))) + } + None => { + let own = p.name == crate::container::CONTAINER || os_engine::effects::from_slot(&p.name, &p.state, 44100).is_some(); + own.then(|| crate::help_links::plugin_page(crate::plugins::slot_label(&p.name))) } - None => os_engine::effects::from_slot(&p.name, &p.state, 44100).map(|_| crate::help_links::BUILTIN_PLUGINS_URL.to_owned()), } } @@ -3828,8 +3831,8 @@ mod tests { assert!(!a.state.mixer.waveforms && a.state.mixer.waves.is_empty()); } - /// The slot menu's Help (Lemon Studio's design): an effect the engine plays itself opens the page on Lemon Studio's - /// own effects; a plugin naming no page says so. + /// The slot menu's Help (Lemon Studio's design): an effect the engine plays itself opens its page in the wiki, under + /// its Lemon Studio name, and so does a container; a plugin naming no page says so. #[test] fn slot_help_opens_the_plugins_page() { let mut a = app(); @@ -3840,7 +3843,9 @@ mod tests { let help = slot_menu(&a, 0, 0.0, 0.0).items.iter().find(|i| i.label == "Help").map(|i| (i.enabled, i.cmd.clone())).unwrap(); assert_eq!(help, (true, Cmd::Mixer(MCmd::SlotHelp(0)))); assert_eq!(run(&mut a, MCmd::SlotHelp(0)).as_deref(), Some("Test FX has no help page")); - assert_eq!(run(&mut a, MCmd::SlotHelp(1)), Some(format!("Opening {}", crate::help_links::BUILTIN_PLUGINS_URL))); + assert_eq!(run(&mut a, MCmd::SlotHelp(1)), Some(format!("Opening {}", crate::help_links::plugin_page("Lemon Balance")))); + track_mut(&mut a.state, 2).unwrap().slots[1] = Some(os_model::SlotPlugin { id: os_model::Id::NONE, name: crate::container::CONTAINER.into(), state: crate::container::encode(&[]) }); + assert_eq!(run(&mut a, MCmd::SlotHelp(1)), Some(format!("Opening {}", crate::help_links::plugin_page(crate::container::CONTAINER)))); } /// FL's measured group drags, in the percentages its hint shows (spec: ui/mixer.md §4): `kept` levels, the pressed diff --git a/crates/os-app/src/playlist.rs b/crates/os-app/src/playlist.rs index 4db2c6d23..13d35bb74 100644 --- a/crates/os-app/src/playlist.rs +++ b/crates/os-app/src/playlist.rs @@ -4463,8 +4463,7 @@ pub fn run(app: &mut App, cmd: Cmd) -> Option { return None; } Cmd::Web(url) => { - crate::help_links::open(url); - app.state.message = Some(format!("Opening {url}")); + app.open_web(url); return None; } Cmd::MidiImport(piano) => { diff --git a/crates/os-app/src/views/menus.rs b/crates/os-app/src/views/menus.rs index e46e89c99..137a83666 100644 --- a/crates/os-app/src/views/menus.rs +++ b/crates/os-app/src/views/menus.rs @@ -223,9 +223,9 @@ pub const ALIGN_LABEL: &str = "Align all channel editors"; pub const OPTIONS_MENU: usize = 5; /// OPTIONS > Project info (the toolbar's info button). pub const OPT_INFO: usize = 9; -/// HELP > About (the toolbar logo). +/// HELP > About (the toolbar logo, the Help button's right click, the macOS application menu). pub const HELP_MENU: usize = 7; -pub const HELP_ABOUT: usize = 7; +pub const HELP_ABOUT: usize = 11; /// Index of the ADD and TOOLS menus. pub const ADD_MENU: usize = 2; pub const TOOLS_MENU: usize = 6; @@ -433,12 +433,16 @@ const TOOLS_B: &[Item] = &[ const HELP: &[Item] = &[ it("Help index", "").on().cmd(MenuCmd::Web(crate::help_links::HELP_INDEX_URL)), it("Tutorials", "").sub().cmd(MenuCmd::Sub(Sub::Tutorials)), + it("Keyboard shortcuts", "").on().cmd(MenuCmd::Web(crate::help_links::SHORTCUTS_URL)), head("Support"), + it("Troubleshooting", "").on().cmd(MenuCmd::Web(crate::help_links::TROUBLESHOOTING_URL)), + it("Report a bug", "").on().cmd(MenuCmd::Web(crate::help_links::REPORT_BUG_URL)), 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(), + it("Lemon Studio website", "").on().cmd(MenuCmd::Web(crate::help_links::WEBSITE_URL)), + it("About...", "").cmd(MenuCmd::Settings(crate::views::settings::ABOUT)), ]; pub const MENUS: &[Menu] = &[ @@ -1134,10 +1138,10 @@ mod tests { } /// Every entry that works says what it does (`MenuCmd`), but for those dispatched by their place (FILE's first - /// entries, About): none falls through to "not available yet", whatever its label. + /// entries): none falls through to "not available yet", whatever its label. #[test] fn working_entries_have_their_command() { - let by_place = [(0, FILE_NEW), (0, FILE_OPEN), (0, FILE_SAVE), (0, FILE_SAVE_AS), (0, FILE_SAVE_VERSION), (0, FILE_EXPORT), (0, FILE_EXPORT_MP3), (0, FILE_EXPORT_MIDI), (0, FILE_EXIT), (HELP_MENU, HELP_ABOUT)]; + let by_place = [(0, FILE_NEW), (0, FILE_OPEN), (0, FILE_SAVE), (0, FILE_SAVE_AS), (0, FILE_SAVE_VERSION), (0, FILE_EXPORT), (0, FILE_EXPORT_MP3), (0, FILE_EXPORT_MIDI), (0, FILE_EXIT)]; for (mi, m) in MENUS.iter().enumerate() { for (i, it) in m.cols.iter().flat_map(|(_, items)| items.iter()).enumerate().filter(|(_, it)| it.kind == Kind::Item) { if works(mi, it) && !by_place.contains(&(mi, i)) { diff --git a/crates/os-app/src/views/settings.rs b/crates/os-app/src/views/settings.rs index a84d58a4f..7b7cb6785 100644 --- a/crates/os-app/src/views/settings.rs +++ b/crates/os-app/src/views/settings.rs @@ -296,10 +296,17 @@ pub const LIVE: &[&str] = &[ ASSOCIATE_CHECK, SHOW_ASIO_PANEL, CHECK_UPDATES, + WEBSITE, + WIKI, + SOURCE_CODE, ]; /// 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: open the project's pages (`crate::help_links`). +pub const WEBSITE: &str = "Website"; +pub const WIKI: &str = "Wiki"; +pub const SOURCE_CODE: &str = "Source code"; /// About: which build this is and how it is kept up to date (`crate::update`). fn update_lines() -> Vec { @@ -426,6 +433,7 @@ pub const THEME: usize = 6; pub const PROJECT: usize = 7; pub const INFO: usize = 8; pub const DEBUG: usize = 9; +pub const ABOUT: usize = 10; /// Plugin table rows shown at once (spec: ui/plugin-manager.md §2). pub const TABLE_ROWS: usize = 18; @@ -957,9 +965,10 @@ pub fn page(tab: usize, s: &State) -> Vec { g("", [ vec![ Gap(20.0), - Big(format!("Lemon Studio {}", crate::version::text())), + Big(format!("Lemon Studio {} (pre-alpha)", crate::version::text())), txt("A digital audio workstation written in Rust."), txt("Copyright (C) 2026 ItsAlphaNeon and the Lemon Studio contributors."), + Buttons(vec![(WEBSITE, 80.0), (WIKI, 64.0), (SOURCE_CODE, 100.0)]), Gap(10.0), ], update_lines(), diff --git a/docs/status.md b/docs/status.md index df01d17ac..06c960f13 100644 --- a/docs/status.md +++ b/docs/status.md @@ -328,10 +328,13 @@ the workspace with the Piano roll and Mixer side by side under it; Save custom l Custom brings it back. The Tablet layouts are greyed (they need the toolbar at the bottom). VIEW > Background > Set image wallpaper... makes a PNG or JPEG picture the theme's workspace wallpaper; the HTML document and the background colour are greyed. VIEW > Align all channel editors (Shift+F12) puts the channel window back right of the Channel -rack and cascades the docked plugin windows of rack channels from there, each title showing. HELP > Help index and -What's new open the project's README and its merged pull requests in the web browser, and Tutorials ▸ the README's -how-to sections; HELP > Diagnostics writes a plain-text report (build, system, audio settings, plugins found, open -project) to Diagnostics.txt in the user folder and opens it. TOOLS > Save MIDI clipboard to browser writes the piano +rack and cascades the docked plugin windows of rack channels from there, each title showing. HELP > Help index, +Tutorials ▸, Keyboard shortcuts, Troubleshooting and Lemon Studio website open the website's pages in the web browser, +as does the toolbar's Help button; What's new and Report a bug open the GitHub releases and the bug report form, and +a built-in plugin's Help opens its page in the wiki. HELP > Diagnostics writes a plain-text report (build, system, +audio settings, plugins found, open project) to Diagnostics.txt in the user folder and opens it. HELP > About..., the +toolbar's logo and a right click on its Help button open the About page of the settings. TOOLS > Save MIDI clipboard +to browser writes the piano roll's copied notes to a numbered MIDI file in the user folder's Clipboard files, which the browser then shows; Save audio clipboard to browser does the same with the audio on the clipboard (what Tesla copies; Windows and macOS). VIEW > Remote control opens the browser's Remote control section, one row per controller link. From 9b6f45a73e4ad185dde259d2da707ecb6b4bb7e6 Mon Sep 17 00:00:00 2001 From: AlphaNeon <102443376+ItsAlphaNeon@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:25:59 -0700 Subject: [PATCH 3/5] Releases: safe to merge, can be rehearsed, and a 0.x tag is an alpha The schedule only runs with the repository variable NIGHTLIES set to "on", so merging the workflow with the signing keys set publishes nothing by itself. Run workflow gains a dry run: it builds, packages and signs, and keeps the files as the run's artifacts. It can rehearse a tag as well as a nightly. An alpha is a plain vMAJOR.MINOR.PATCH tag with major version 0. Builds say so in the app, the diagnostics and the release's title, instead of "pre-alpha". Also: a tag has to be on main; the first build on a channel doesn't list every pull request ever merged; appimagetool is a fixed version with its checksum; a nightly and an alpha on one computer keep their downloads in separate folders. The plan moves to tools/package/plan.sh, tested with the manifest by tools/package/test.sh in CI, and CI checks a nightly's and a tagged build's settings compile. Co-Authored-By: Claude Opus 5.5 --- .gitattributes | 3 + .github/workflows/ci.yml | 17 ++++ .github/workflows/release.yml | 99 ++++++++++---------- crates/os-app/src/diagnostics.rs | 4 +- crates/os-app/src/update.rs | 8 +- crates/os-app/src/version.rs | 53 ++++++++++- crates/os-app/src/views/mod.rs | 2 +- crates/os-app/src/views/settings.rs | 10 +-- tools/package/plan.sh | 75 ++++++++++++++++ tools/package/test.sh | 135 ++++++++++++++++++++++++++++ tools/updater/src/manifest.rs | 6 +- 11 files changed, 346 insertions(+), 66 deletions(-) create mode 100755 tools/package/plan.sh create mode 100755 tools/package/test.sh 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 index cbad25942..1b2583260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,9 +3,13 @@ # - 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. -# - Stable: a pushed tag vMAJOR.MINOR.PATCH, which must match the workspace's version in Cargo.toml. Its release holds -# stable.json; GitHub's "latest" release is the newest stable 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 @@ -24,6 +28,14 @@ on: 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 @@ -39,6 +51,8 @@ env: 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 }} @@ -66,45 +80,9 @@ jobs: id: plan env: FORCE: ${{ inputs.force }} - run: | - set -euo pipefail - sha=$(git rev-parse HEAD) - skip=false - if [[ "$GITHUB_REF" == refs/tags/v* ]]; then - channel=stable - tag=$GITHUB_REF_NAME - version=${tag#v} - workspace=$(sed -n '/^\[workspace.package\]/,/^\[/s/^version = "\(.*\)"/\1/p' Cargo.toml) - if [ "$version" != "$workspace" ]; then - echo "::error::The tag $tag does not match the workspace version $workspace in Cargo.toml" - exit 1 - fi - previous=$( (git tag -l 'v*.*.*' --sort=-v:refname | grep -vx "$tag" || true) | head -n1) - else - if [ "$GITHUB_REF" != refs/heads/main ]; then - echo "::error::Nightlies are built from main (this run is on $GITHUB_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 ]; then - echo "main has not changed since $previous: no nightly" - skip=true - fi - fi - echo "$channel $version ($tag at $sha), after ${previous:-nothing}" - { - echo "channel=$channel" - echo "version=$version" - echo "tag=$tag" - echo "previous=$previous" - echo "sha=$sha" - echo "skip=$skip" - } >>"$GITHUB_OUTPUT" + DRY_RUN: ${{ inputs.dry_run }} + REHEARSE_TAG: ${{ inputs.rehearse_tag }} + run: bash tools/package/plan.sh build: name: ${{ matrix.name }} @@ -176,8 +154,10 @@ jobs: cp target/release/lemonstudio.exe "dist/LemonStudio-$v-$PLATFORM.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/continuous/appimagetool-x86_64.AppImage + 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/LemonStudio-$v-$PLATFORM.AppImage" @@ -228,14 +208,18 @@ jobs: - name: Release notes run: | set -euo pipefail - args=(-f "tag_name=$TAG" -f "target_commitish=$SHA") - [ -n "$PREVIOUS" ] && args+=(-f "previous_tag_name=$PREVIOUS") - gh api "repos/$GITHUB_REPOSITORY/releases/generate-notes" "${args[@]}" --jq .body >"$RUNNER_TEMP/changes.md" - if [ "$CHANNEL" = nightly ]; then - intro="A nightly build of main at ${SHA:0:8}: the newest changes, and the least tested. Lemon Studio is in its pre-alpha." + # 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 - intro="Lemon Studio $VERSION." + 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." ;; + 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() @@ -263,11 +247,24 @@ jobs: 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 - gh release create "$TAG" dist/* --verify-tag --title "Lemon Studio $VERSION" --notes-file "$RUNNER_TEMP/notes.md" + 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" @@ -280,7 +277,7 @@ jobs: fi - name: Delete old nightlies - if: needs.plan.outputs.channel == 'nightly' + if: needs.plan.outputs.channel == 'nightly' && !inputs.dry_run run: | set -euo pipefail gh release list --limit 1000 --json tagName --jq '.[].tagName' \ diff --git a/crates/os-app/src/diagnostics.rs b/crates/os-app/src/diagnostics.rs index e71e3938b..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)", crate::version::text()), + 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", crate::version::text())), "{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/update.rs b/crates/os-app/src/update.rs index 7633a505a..2973398e0 100644 --- a/crates/os-app/src/update.rs +++ b/crates/os-app/src/update.rs @@ -50,9 +50,11 @@ pub fn enabled() -> bool { CHANNEL.is_some() && UPDATER.is_some() } -/// `/Updates`: the updater, the download, the outcome of the last update. +/// `/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 { - os_paths::user_file("Updates") + 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 @@ -443,7 +445,7 @@ impl crate::app::App { other => { self.updates.install_when_staged = false; self.updates.asked = false; - let channel = CHANNEL.unwrap_or(""); + 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."), diff --git a/crates/os-app/src/version.rs b/crates/os-app/src/version.rs index 9252ad777..639e8fa9e 100644 --- a/crates/os-app/src/version.rs +++ b/crates/os-app/src/version.rs @@ -3,9 +3,10 @@ // 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 stable -//! release's is semantic, `MAJOR.MINOR.PATCH` (the workspace's `version`). A build from source has neither: it is the -//! workspace's version and belongs to no channel, so it never updates itself (`crate::update`). +//! (`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") { @@ -25,6 +26,38 @@ pub fn text() -> &'static str { }) } +/// 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::*; @@ -35,4 +68,18 @@ mod tests { 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/mod.rs b/crates/os-app/src/views/mod.rs index bab668bd8..b0842fd57 100644 --- a/crates/os-app/src/views/mod.rs +++ b/crates/os-app/src/views/mod.rs @@ -184,7 +184,7 @@ 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 = format!("Lemon Studio v{} - pre-alpha", crate::version::text()); + 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); diff --git a/crates/os-app/src/views/settings.rs b/crates/os-app/src/views/settings.rs index 22c4dcf5c..54954bd73 100644 --- a/crates/os-app/src/views/settings.rs +++ b/crates/os-app/src/views/settings.rs @@ -304,11 +304,11 @@ 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.", - (Some(_), true) => "It updates itself from the stable releases.", - _ => "Built from its source code: it doesn't update itself (git pull, then build it again).", + (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)])] + 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. @@ -957,7 +957,7 @@ pub fn page(tab: usize, s: &State) -> Vec { g("", [ vec![ Gap(20.0), - Big(format!("Lemon Studio {}", crate::version::text())), + 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), 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/test.sh b/tools/package/test.sh new file mode 100755 index 000000000..fd86407d2 --- /dev/null +++ b/tools/package/test.sh @@ -0,0 +1,135 @@ +#!/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-$v-windows-x86_64.exe" "LemonStudio-$v-linux-x86_64.AppImage" "LemonStudio-$v-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-0.1.0-linux-x86_64.AppImage" +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 +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/src/manifest.rs b/tools/updater/src/manifest.rs index 675110492..2d6c2a943 100644 --- a/tools/updater/src/manifest.rs +++ b/tools/updater/src/manifest.rs @@ -14,7 +14,8 @@ //! ``` //! //! Versions: a nightly is dated, `YEAR.MONTH.DAY.N` (UTC, no zero padding; `N` counts that day's nightlies from 1); a -//! stable release is semantic, `MAJOR.MINOR.PATCH`. The two are never compared with each other. +//! 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; @@ -135,6 +136,9 @@ mod tests { 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()); } From e127a3f6ac37b126844269903c2d52e198dfdf60 Mon Sep 17 00:00:00 2001 From: AlphaNeon <102443376+ItsAlphaNeon@users.noreply.github.com> Date: Sun, 8 Nov 2026 16:38:42 -0700 Subject: [PATCH 4/5] Docs: going live, and how a tagged release works docs/releasing.md has the steps in order, from the signing key to the first nightly, and says an alpha is a tag with major version 0. The README no longer says no prebuilt binaries will be published. Co-Authored-By: Claude Opus 5.5 --- README.md | 15 ++++---- docs/alpha-checklist.md | 13 ++++--- docs/releasing.md | 84 +++++++++++++++++++++++++++++++++++------ 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 137cf57fd..9a1f025b7 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,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". -> - **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. @@ -118,12 +117,12 @@ Per-area details are in [docs/status.md](docs/status.md). The full, item-by-item ## Getting started -**Nightly builds** for Windows, Linux (AppImage) and macOS (Apple silicon) are on the -[releases page](https://github.com/ItsAlphaNeon/Lemon-Studio/releases): the newest changes on main, built every night, -with Lemon Studio's plugins for other DAWs as a separate zip. They are pre-alpha and not signed: the release notes say -how to open them on each system (on macOS, past Gatekeeper). Once installed, a nightly checks for a newer nightly when -it starts and installs it when you agree (Settings ▸ General ▸ Updates); stable releases will do the same with stable -releases. How releases are made: [docs/releasing.md](docs/releasing.md). +**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 diff --git a/docs/alpha-checklist.md b/docs/alpha-checklist.md index f5877e3b6..d36e18b2d 100644 --- a/docs/alpha-checklist.md +++ b/docs/alpha-checklist.md @@ -211,15 +211,16 @@ 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. Stable releases ship once the rest of this checklist is -done (decided 2026-10-29), so these come last. 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. +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.** The release workflow builds them, a nightly from main every - night and a stable release from a `vMAJOR.MINOR.PATCH` tag: a Windows `.exe`, a Linux AppImage and a macOS `.app` + 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: its first run, and trying the updates on each 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 - [x] **macOS signing and notarization.** Not done: builds and plugin bundles are signed ad hoc only, as notarization diff --git a/docs/releasing.md b/docs/releasing.md index f0437fa13..bd26a7a10 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -5,8 +5,14 @@ build updates itself on its own channel. A build from source has no channel and | Channel | Made by | Version | GitHub release | |---|---|---|---| -| Nightly | Every night at 04:00 UTC from `main`, 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 | -| Stable (after the alpha) | Pushing a tag `vMAJOR.MINOR.PATCH` | Semantic: the workspace's `version` in `Cargo.toml`, which the tag must match | Release `v1.0.0`, holding `stable.json`; GitHub's *latest* 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: @@ -19,7 +25,7 @@ Each release has: 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. +previous release on that channel. The first release on a channel has no such list. ## How a build updates itself @@ -30,11 +36,11 @@ these environment variables set: - `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: +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` -- stable: `releases/latest/download/stable.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 @@ -51,10 +57,15 @@ A copy that can't replace itself says so and doesn't offer the update: 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. -## The signing key (once) +## 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: -Manifests are signed with [minisign](https://jedisct1.github.io/minisign/). Make a key pair without a password; the -GitHub secret store protects it: +### 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 @@ -67,7 +78,56 @@ key. If the key is lost or replaced, the copies already out there can't verify n 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. -## Making a stable release +### 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 -1. Set `version` under `[workspace.package]` in `Cargo.toml`, e.g. `1.0.0`, and merge it. -2. Tag the merge commit and push the tag: `git tag v1.0.0 && git push origin v1.0.0`. +`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. From 0d43faaa339876248d1011be5fb4d87744df2234 Mon Sep 17 00:00:00 2001 From: AlphaNeon <102443376+ItsAlphaNeon@users.noreply.github.com> Date: Sun, 8 Nov 2026 17:16:23 -0700 Subject: [PATCH 5/5] Releases: the program's file name has no version in it LemonStudio.exe and LemonStudio.AppImage, and LemonStudioNightly for a nightly, so a nightly and a tagged release can sit in one folder. An update replaces the file in place, keeping its name, so a version in it went stale after the first update. Now a shortcut never points at a misleading name, and never has to change. The macOS zip keeps its version: it's only unpacked, and the app inside has a fixed name already. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 9 ++++++--- docs/releasing.md | 7 +++++-- tools/package/manifest.py | 34 ++++++++++++++++++++++------------ tools/package/release-notes.md | 9 +++++---- tools/package/test.sh | 22 ++++++++++++++++++++-- 5 files changed, 58 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1b2583260..5394b65a6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -148,10 +148,12 @@ jobs: 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/LemonStudio-$v-$PLATFORM.exe" + cp target/release/lemonstudio.exe "dist/$program.exe" ;; Linux) # A fixed version with its checksum: this goes into a release that installed copies trust. @@ -160,7 +162,7 @@ jobs: 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/LemonStudio-$v-$PLATFORM.AppImage" + bash tools/package/appimage.sh target/release/lemonstudio "$v" "dist/$program.AppImage" ;; macOS) ditto -c -k --keepParent "target/bundled/Lemon Studio.app" "dist/LemonStudio-$v-$PLATFORM.zip" @@ -224,7 +226,8 @@ jobs: import os, sys changes = open(sys.argv[1], encoding="utf-8").read().strip() text = open("tools/package/release-notes.md", encoding="utf-8").read() - print(text.replace("{INTRO}", sys.argv[2]).replace("{VERSION}", os.environ["VERSION"]).replace("{CHANGES}", changes)) + program = "LemonStudioNightly" if os.environ["CHANNEL"] == "nightly" else "LemonStudio" + print(text.replace("{INTRO}", sys.argv[2]).replace("{VERSION}", os.environ["VERSION"]).replace("{PROGRAM}", program).replace("{CHANGES}", changes)) PY cp LICENSE THIRD-PARTY-NOTICES.txt dist/ diff --git a/docs/releasing.md b/docs/releasing.md index bd26a7a10..9c77ebbd2 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -16,13 +16,16 @@ GitHub's *latest* release. In the workflow and the manifest the channel is calle Each release has: -- `LemonStudio--windows-x86_64.exe` -- `LemonStudio--linux-x86_64.AppImage`, built on Ubuntu 22.04 by [tools/package/appimage.sh](../tools/package/appimage.sh) +- `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`, 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. diff --git a/tools/package/manifest.py b/tools/package/manifest.py index 4768db5d9..fffba8664 100644 --- a/tools/package/manifest.py +++ b/tools/package/manifest.py @@ -10,9 +10,9 @@ 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: `LemonStudio--.`, `` as -Rust names it (`windows-x86_64`, `linux-x86_64`, `macos-aarch64`). 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. +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 @@ -39,19 +39,29 @@ def notes(markdown): return out -def assets(dist, version, base): - pattern = re.compile(r"^LemonStudio-" + re.escape(version) + r"-(?P[a-z0-9]+-[a-z0-9_]+)\.(exe|AppImage|zip)$") +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 name in sorted(os.listdir(dist)): - m = pattern.match(name) - if not m: - continue + 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[m.group("platform")] = {"url": f"{base}/{name}", "size": os.path.getsize(path), "sha256": h.hexdigest()} + found[platform] = {"url": f"{base}/{name}", "size": os.path.getsize(path), "sha256": h.hexdigest()} return found @@ -65,9 +75,9 @@ def main(): 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.version, f"https://github.com/{a.repo}/releases/download/{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"no LemonStudio-{a.version}- programs in {a.dist}") + sys.exit(f"none of the programs in {a.dist}: {', '.join(programs(a.channel, a.version).values())}") manifest = { "channel": a.channel, "version": a.version, diff --git a/tools/package/release-notes.md b/tools/package/release-notes.md index 7bf084122..739eb17a3 100644 --- a/tools/package/release-notes.md +++ b/tools/package/release-notes.md @@ -4,16 +4,17 @@ | | Lemon Studio | Its plugins for other DAWs (VST3 and CLAP) | |---|---|---| -| Windows | `LemonStudio-{VERSION}-windows-x86_64.exe` | `LemonStudio-Plugins-{VERSION}-windows-x86_64.zip` | -| Linux | `LemonStudio-{VERSION}-linux-x86_64.AppImage` | `LemonStudio-Plugins-{VERSION}-linux-x86_64.zip` | +| 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. +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 LemonStudio-*.AppImage`, or in its file properties) and run it. +- **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 **Lemon Studio.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 ▸ diff --git a/tools/package/test.sh b/tools/package/test.sh index fd86407d2..44c02b51c 100755 --- a/tools/package/test.sh +++ b/tools/package/test.sh @@ -99,7 +99,8 @@ tag() { echo "GITHUB_REF=refs/tags/$1 GITHUB_REF_NAME=$1"; } # 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-$v-windows-x86_64.exe" "LemonStudio-$v-linux-x86_64.AppImage" "LemonStudio-$v-macos-aarch64.zip" \ +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 @@ -110,7 +111,9 @@ 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-0.1.0-linux-x86_64.AppImage" +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 @@ -119,6 +122,21 @@ 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