diff --git a/.github/actions/setup-rust/action.yml b/.github/actions/setup-rust/action.yml new file mode 100644 index 00000000..4115cc0c --- /dev/null +++ b/.github/actions/setup-rust/action.yml @@ -0,0 +1,32 @@ +name: Setup Rust +description: Install the Rust toolchain (+ optional targets) and restore the build cache. +inputs: + target: + description: Comma-separated extra target triples to install + required: false + default: '' + save-cache: + description: Whether to save the Rust cache + required: false + default: 'false' + restore-cache: + description: Whether to restore the Rust cache + required: false + default: 'true' +runs: + using: composite + steps: + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + targets: ${{ inputs.target }} + components: cargo, clippy, rustc, rustfmt + + - name: Cache Rust + if: ${{ inputs.restore-cache == 'true' }} + uses: Swatinem/rust-cache@master + with: + key: ${{ inputs.target }} + save-if: ${{ inputs.save-cache }} + shared-key: release-cache diff --git a/.github/workflows/oneclient_release.yml b/.github/workflows/oneclient_release.yml new file mode 100644 index 00000000..599beced --- /dev/null +++ b/.github/workflows/oneclient_release.yml @@ -0,0 +1,293 @@ +name: OneClient Release Build + + +on: + workflow_dispatch: + +env: + CARGO_PACKAGER_SIGN_PRIVATE_KEY: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY }} + CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD: ${{ secrets.CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD }} + +jobs: + prepare: + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + version: ${{ steps.version.outputs.version }} + tag: ${{ steps.version.outputs.tag }} + steps: + - uses: actions/checkout@v4 + + - name: Resolve version from Cargo.toml + id: version + shell: bash + run: | + VERSION=$(grep -m1 '^version' Cargo.toml | sed -E 's/.*"([^"]+)".*/\1/') + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "tag=oneclient-$VERSION" >> "$GITHUB_OUTPUT" + + - name: Create/ensure draft release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + VERSION: ${{ steps.version.outputs.version }} + run: | + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" \ + --draft \ + --title "OneClient $VERSION" \ + --notes "Automated release for OneClient $VERSION." + fi + + # --------------------------------------------------------------------------- + package: + needs: prepare + permissions: + contents: write + # Required so SignPath can download the uploaded build artifact. + actions: read + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + target: aarch64-apple-darwin + os_key: darwin + arch: aarch64 + formats: app,dmg + - platform: macos-latest + target: x86_64-apple-darwin + os_key: darwin + arch: x86_64 + formats: app,dmg + - platform: ubuntu-22.04 + target: '' + os_key: linux + arch: x86_64 + formats: deb,appimage + - platform: windows-latest + target: '' + os_key: windows + arch: x86_64 + formats: nsis + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v4 + + - name: Setup Rust + uses: ./.github/actions/setup-rust + with: + target: ${{ matrix.target }} + save-cache: true + restore-cache: true + + - name: (Linux) Install system dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + build-essential cmake pkg-config clang python3 \ + libfontconfig-dev libfreetype-dev \ + libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \ + libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libdbus-1-dev \ + libfuse2 + + - name: Install cargo-packager + shell: bash + run: cargo install cargo-packager --version 0.11.8 --locked + + - name: Import Apple Developer certificate + if: startsWith(matrix.platform, 'macos') + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + CERT_ID=$(security find-identity -v -p codesigning build.keychain | awk -F'"' '/Developer ID Application/ {print $2; exit}') + if [ -z "$CERT_ID" ]; then + echo "::error::No Developer ID Application certificate found in keychain." + exit 1 + fi + echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV" + + - name: Build release binary + shell: bash + run: | + if [ -n "${{ matrix.target }}" ]; then + cargo build --release --bin oneclient_app --target ${{ matrix.target }} + else + cargo build --release --bin oneclient_app + fi + + - name: Package + shell: bash + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + ARGS="--release --formats ${{ matrix.formats }}" + if [ -n "${{ matrix.target }}" ]; then + ARGS="$ARGS --target ${{ matrix.target }}" + fi + cargo packager $ARGS --verbose + + - name: Resolve packager output dir + id: outdir + shell: bash + run: | + if [ -n "${{ matrix.target }}" ]; then + echo "dir=target/${{ matrix.target }}/release" >> "$GITHUB_OUTPUT" + else + echo "dir=target/release" >> "$GITHUB_OUTPUT" + fi + + - name: (Windows) Locate installer + if: matrix.platform == 'windows-latest' + id: win_installer + shell: bash + run: echo "path=$(ls ${{ steps.outdir.outputs.dir }}/*-setup.exe | head -n1)" >> "$GITHUB_OUTPUT" + + - name: (Windows) Upload unsigned installer artifact + if: matrix.platform == 'windows-latest' + id: win_upload_unsigned + uses: actions/upload-artifact@v4 + with: + name: oneclient-windows-installer-unsigned + path: ${{ steps.win_installer.outputs.path }} + + - name: (Windows) Submit SignPath signing request + if: matrix.platform == 'windows-latest' + uses: signpath/github-action-submit-signing-request@v1 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }} + project-slug: onelauncher + signing-policy-slug: test-signing + artifact-configuration-slug: onelauncher-installer + github-artifact-id: ${{ steps.win_upload_unsigned.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed + + - name: (Windows) Replace installer with signed one + recompute updater sig + if: matrix.platform == 'windows-latest' + shell: bash + run: | + signed="$(ls signed/*-setup.exe | head -n1)" + # Overwrite the unsigned installer in the packager out dir with the signed exe. + cp "$signed" "${{ steps.win_installer.outputs.path }}" + # Recompute the updater signature over the SIGNED exe. + cargo packager signer sign \ + --private-key "$CARGO_PACKAGER_SIGN_PRIVATE_KEY" \ + --password "$CARGO_PACKAGER_SIGN_PRIVATE_KEY_PASSWORD" \ + "${{ steps.win_installer.outputs.path }}" + + - name: Upload installers and build manifest fragment + shell: bash + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.prepare.outputs.tag }} + OUT_DIR: ${{ steps.outdir.outputs.dir }} + OS_KEY: ${{ matrix.os_key }} + ARCH: ${{ matrix.arch }} + VERSION: ${{ needs.prepare.outputs.version }} + run: | + set -euo pipefail + shopt -s nullglob + + # Identify the single updater artifact for this platform (the file with a + # sibling .sig): -setup.exe (nsis), *.app.tar.gz (app), *.AppImage.tar.gz (appimage). + updater_file=""; fmt=""; ext="" + case "$OS_KEY" in + windows) updater_file="$(ls "$OUT_DIR"/*-setup.exe | head -n1)"; fmt="nsis"; ext="exe" ;; + darwin) updater_file="$(ls "$OUT_DIR"/*.app.tar.gz | head -n1)"; fmt="app"; ext="app.tar.gz" ;; + linux) updater_file="$(ls "$OUT_DIR"/*.AppImage.tar.gz 2>/dev/null | head -n1)"; fmt="appimage"; ext="AppImage.tar.gz" + if [ -z "$updater_file" ]; then updater_file="$(ls "$OUT_DIR"/*.AppImage | head -n1)"; ext="AppImage"; fi ;; + esac + + # The updater bundle isn't always arch-tagged by cargo-packager, so the two + # macOS legs would collide on the release. Rename the updater artifact + its .sig + # to an arch-unique name before upload, and point the manifest at that name. + unique="OneClient_${VERSION}_${OS_KEY}_${ARCH}.${ext}" + cp "$updater_file" "$OUT_DIR/$unique" + cp "$updater_file.sig" "$OUT_DIR/$unique.sig" + + # Upload human-facing installers (dmg/deb/AppImage/signed exe) + the renamed + # updater bundle and its .sig. Skip the un-renamed updater bundle to avoid clashes. + for f in "$OUT_DIR"/*.dmg "$OUT_DIR"/*.deb "$OUT_DIR"/*.AppImage "$OUT_DIR"/*-setup.exe "$OUT_DIR/$unique" "$OUT_DIR/$unique.sig"; do + [ -e "$f" ] || continue + gh release upload "$TAG" "$f" --clobber + done + + base="$unique" + mkdir -p fragments + SIG="$(cat "$updater_file.sig")" \ + URL="https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/${base}" \ + FMT="$fmt" \ + KEY="${OS_KEY}-${ARCH}" \ + python3 - <<'PY' + import json, os + frag = { + os.environ["KEY"]: { + "signature": os.environ["SIG"], + "url": os.environ["URL"], + "format": os.environ["FMT"], + } + } + with open(f"fragments/{os.environ['KEY']}.json", "w") as f: + json.dump(frag, f) + PY + + - name: Upload manifest fragment + uses: actions/upload-artifact@v4 + with: + name: manifest-${{ matrix.os_key }}-${{ matrix.arch }} + path: fragments/*.json + + manifest: + needs: [prepare, package] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download all manifest fragments + uses: actions/download-artifact@v4 + with: + pattern: manifest-* + path: fragments + merge-multiple: true + + - name: Assemble latest.json + env: + VERSION: ${{ needs.prepare.outputs.version }} + run: | + python3 - <<'PY' + import json, glob, os, datetime + platforms = {} + for path in glob.glob("fragments/*.json"): + with open(path) as f: + platforms.update(json.load(f)) + manifest = { + "version": os.environ["VERSION"], + "pub_date": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "platforms": platforms, + } + with open("latest.json", "w") as f: + json.dump(manifest, f, indent=2) + print(json.dumps(manifest, indent=2)) + PY + + - name: Upload latest.json to release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.prepare.outputs.tag }} + run: gh release upload "$TAG" latest.json --clobber diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2b0be4a6..4d0b3af4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -55,6 +55,19 @@ The app crate is `oneclient_app`: After you finish making your changes and committed them to your branch, run `cargo fmt` and `cargo clippy` to fix style inconsistencies and catch lints. +### Packaging + +Installers are built with [cargo-packager] (config in +`packages/oneclient_app/Cargo.toml` under `[package.metadata.packager]`): + +- `cargo install cargo-packager --locked` +- `cargo build --release -p oneclient_app` +- `cargo packager --release -p oneclient_app --formats ` + +Releases are cut by dispatching the `OneClient Release Build` GitHub Actions +workflow. See the **Packaging / Releasing** section of the [README] for details +and per-OS prerequisites. + ### Pull Request Once you have finished making your changes, create a pull request (PR) to submit them. @@ -84,6 +97,7 @@ Run `xcode-select --install` in the terminal to install them. If they are alread This CONTRIBUTING.md file was inspired by the [`github/docs` CONTRIBUTING.md] file, and we extend our gratitude to the original authors. +[cargo-packager]: https://github.com/crabnebula-dev/cargo-packager [Skia]: https://skia.org/ [Freya setup guide]: https://book.freyaui.dev/setup.html [`cargo`]: https://doc.rust-lang.org/cargo/getting-started/installation.html diff --git a/Cargo.lock b/Cargo.lock index 140fd4dc..9b1c5367 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -931,6 +931,42 @@ dependencies = [ "wayland-client", ] +[[package]] +name = "cargo-packager-updater" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec09acab5c2227aba2e592d431708305bdeb6d507703f6cd8983fb57b6c5ef7" +dependencies = [ + "base64", + "cargo-packager-utils", + "ctor 0.2.9", + "dirs", + "dunce", + "flate2", + "http", + "log", + "minisign-verify", + "percent-encoding", + "reqwest 0.12.28", + "semver", + "serde", + "serde_json", + "tar", + "tempfile", + "thiserror 1.0.69", + "time", + "url", +] + +[[package]] +name = "cargo-packager-utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b43458dd2ee3cdab3f5b105acd80791383b730380c929018701313d7d299d4e8" +dependencies = [ + "ctor 0.2.9", +] + [[package]] name = "cc" version = "1.2.63" @@ -1304,6 +1340,16 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn", +] + [[package]] name = "ctor" version = "0.10.1" @@ -1346,6 +1392,12 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + [[package]] name = "digest" version = "0.10.7" @@ -1373,7 +1425,28 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1384,7 +1457,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.2", "windows-sys 0.61.2", ] @@ -2606,6 +2679,7 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "tokio", "tokio-rustls", "tower-service", @@ -3359,6 +3433,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" @@ -3530,6 +3610,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + [[package]] name = "num-derive" version = "0.4.2" @@ -4020,6 +4106,7 @@ dependencies = [ "anyhow", "arboard", "bytes 1.11.1", + "cargo-packager-updater", "chrono", "freya", "freya-skia-safe", @@ -4064,7 +4151,7 @@ dependencies = [ "parking_lot", "polyio", "rand 0.10.1", - "reqwest", + "reqwest 0.13.3", "sanitize-filename", "serde", "serde_json", @@ -4379,6 +4466,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -4757,6 +4850,17 @@ dependencies = [ "bitflags 2.12.1", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.2" @@ -4797,6 +4901,48 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64", + "bytes 1.11.1", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + [[package]] name = "reqwest" version = "0.13.3" @@ -4841,7 +4987,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] @@ -5868,6 +6014,36 @@ dependencies = [ "zune-jpeg", ] +[[package]] +name = "time" +version = "0.3.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tiny-skia" version = "0.11.4" @@ -5900,7 +6076,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90a0ca3ee6a69f2ad28fd11621a4c3f03b371f366be500b64df260c4ffbafb4" dependencies = [ "as-raw-xcb-connection", - "ctor", + "ctor 0.10.1", "libloading", "pkg-config", "tracing", @@ -6538,6 +6714,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -6950,6 +7139,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -6977,6 +7175,21 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -7019,6 +7232,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -7031,6 +7250,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -7043,6 +7268,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -7067,6 +7298,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -7079,6 +7316,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -7091,6 +7334,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -7103,6 +7352,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/README.md b/README.md index 50f159ff..d816872e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,22 @@ cargo run -p oneclient_app cargo build --release -p oneclient_app ``` +### Packaging / Releasing + +Installers are produced with [**cargo-packager**](https://github.com/crabnebula-dev/cargo-packager) +(the standalone bundler spun out of the Tauri bundler). Config lives in +[`packages/oneclient_app/Cargo.toml`](./packages/oneclient_app/Cargo.toml) under +`[package.metadata.packager]`. + +```sh +cargo install cargo-packager --locked + +# Build the binary, then bundle it for the current OS: +cargo build --release -p oneclient_app +cargo packager --release -p oneclient_app --formats +# Windows: nsis macOS: app,dmg Linux: deb,appimage +``` + ### Versioning The workspace shares a single version, defined in the root [`Cargo.toml`](./Cargo.toml) diff --git a/packages/oneclient_app/Cargo.toml b/packages/oneclient_app/Cargo.toml index 61b5e5d2..b1eac8f1 100644 --- a/packages/oneclient_app/Cargo.toml +++ b/packages/oneclient_app/Cargo.toml @@ -32,6 +32,8 @@ sysinfo.workspace = true anyhow.workspace = true regex.workspace = true +cargo-packager-updater = "=0.2.3" + # file dialog rfd = "=0.17.2" @@ -48,3 +50,43 @@ mimalloc = "=0.1.52" [patch.crates-io] freya = { git = "https://github.com/marc2332/freya.git", branch = "fix/blur" } +# Build then package (per OS): +# cargo build --release -p oneclient_app +# cargo packager --release -p oneclient_app --formats +[package.metadata.packager] +product-name = "OneClient" +identifier = "org.polyfrost.OneClient" +category = "Game" +publisher = "Polyfrost" +copyright = "© 2026 Polyfrost Inc." +homepage = "https://polyfrost.org/projects/oneclient" +license-file = "../../LICENSE" +long-description = "Next-generation open source Minecraft launcher" +formats = ["nsis", "app", "dmg", "deb", "appimage"] +binaries = [{ path = "oneclient_app", main = true }] +icons = [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico", +] + +[package.metadata.packager.macos] +minimum-system-version = "10.15" +entitlements = "distribution/App.entitlements" + +[package.metadata.packager.dmg] +app-position = { x = 190, y = 190 } +app-folder-position = { x = 470, y = 190 } + +[package.metadata.packager.windows] +digest-algorithm = "sha256" +timestamp-url = "https://timestamp.digicert.com" + +[package.metadata.packager.deb] +desktop-template = "distribution/linux/org.polyfrost.oneclient.template" + +[package.metadata.packager.appimage] +# desktop entry generated from the deb template above via the linux config + diff --git a/packages/oneclient_app/distribution/App.entitlements b/packages/oneclient_app/distribution/App.entitlements new file mode 100644 index 00000000..48f4264c --- /dev/null +++ b/packages/oneclient_app/distribution/App.entitlements @@ -0,0 +1,18 @@ + + + + + + com.apple.security.cs.disable-library-validation + + + com.apple.security.cs.allow-dyld-environment-variables + + + com.apple.security.device.audio-input + + + com.apple.security.device.camera + + + diff --git a/packages/oneclient_app/distribution/linux/org.polyfrost.oneclient.template b/packages/oneclient_app/distribution/linux/org.polyfrost.oneclient.template new file mode 100644 index 00000000..e43eb2ad --- /dev/null +++ b/packages/oneclient_app/distribution/linux/org.polyfrost.oneclient.template @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=OneClient +Comment=Next-generation open source Minecraft launcher +Exec={{exec}} +Icon={{icon}} +Type=Application +Terminal=false +StartupNotify=true +Categories=Game;ActionGame;AdventureGame;Simulation; +Keywords=mc;minecraft;game +StartupWMClass={{exec}} +Version=1.4 diff --git a/packages/oneclient_app/icons/128x128.png b/packages/oneclient_app/icons/128x128.png new file mode 100644 index 00000000..21b90752 Binary files /dev/null and b/packages/oneclient_app/icons/128x128.png differ diff --git a/packages/oneclient_app/icons/128x128@2x.png b/packages/oneclient_app/icons/128x128@2x.png new file mode 100644 index 00000000..0786a43c Binary files /dev/null and b/packages/oneclient_app/icons/128x128@2x.png differ diff --git a/packages/oneclient_app/icons/32x32.png b/packages/oneclient_app/icons/32x32.png new file mode 100644 index 00000000..6c92d512 Binary files /dev/null and b/packages/oneclient_app/icons/32x32.png differ diff --git a/packages/oneclient_app/icons/512x512.png b/packages/oneclient_app/icons/512x512.png new file mode 100644 index 00000000..fbc4ca47 Binary files /dev/null and b/packages/oneclient_app/icons/512x512.png differ diff --git a/packages/oneclient_app/icons/StoreLogo.png b/packages/oneclient_app/icons/StoreLogo.png new file mode 100644 index 00000000..431f57e5 Binary files /dev/null and b/packages/oneclient_app/icons/StoreLogo.png differ diff --git a/packages/oneclient_app/icons/icon.icns b/packages/oneclient_app/icons/icon.icns new file mode 100644 index 00000000..3f4729d4 Binary files /dev/null and b/packages/oneclient_app/icons/icon.icns differ diff --git a/packages/oneclient_app/icons/icon.ico b/packages/oneclient_app/icons/icon.ico new file mode 100644 index 00000000..446f9a84 Binary files /dev/null and b/packages/oneclient_app/icons/icon.ico differ diff --git a/packages/oneclient_app/icons/icon.png b/packages/oneclient_app/icons/icon.png new file mode 100644 index 00000000..fbc4ca47 Binary files /dev/null and b/packages/oneclient_app/icons/icon.png differ diff --git a/packages/oneclient_app/src/lib.rs b/packages/oneclient_app/src/lib.rs index 3e2ed858..066daa30 100644 --- a/packages/oneclient_app/src/lib.rs +++ b/packages/oneclient_app/src/lib.rs @@ -10,6 +10,7 @@ mod notifications; pub mod platform; mod routes; pub mod theme; +pub mod updater; mod ui; pub(crate) mod utils; mod view; diff --git a/packages/oneclient_app/src/main.rs b/packages/oneclient_app/src/main.rs index 76bad9d8..d026dc66 100644 --- a/packages/oneclient_app/src/main.rs +++ b/packages/oneclient_app/src/main.rs @@ -46,6 +46,8 @@ fn main() { oneclient_core::logger::init() }.expect("Failed to initialize logger"); + oneclient_app::updater::spawn_update_check(settings.auto_update); + let (bridge, handle): (OneClientBridge, CoreBridgeHandle) = OneClientBridge::new(); handle.spawn_runtime(); diff --git a/packages/oneclient_app/src/updater.rs b/packages/oneclient_app/src/updater.rs new file mode 100644 index 00000000..a6504f1f --- /dev/null +++ b/packages/oneclient_app/src/updater.rs @@ -0,0 +1,42 @@ +//! In-app self-update via [`cargo-packager-updater`]. + +use cargo_packager_updater::{Config, check_update}; + +pub const UPDATER_PUBKEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFGODk3MkMyMjg0MjFDMDUKUldRRkhFSW93bktKSHpkWjNEMXNzaDVINVpCTU8xSnhuK2RnV0dTZ2FkcFJWbG1zUkhGYTNjaUkK"; + +pub const UPDATER_ENDPOINT: &str = + "https://github.com/Polyfrost/OneLauncher/releases/latest/download/latest.json"; + +pub fn spawn_update_check(auto_install: bool) { + std::thread::Builder::new() + .name("oneclient-updater".into()) + .spawn(move || { + if let Err(err) = run_check(auto_install) { + tracing::warn!("update check failed: {err}"); + } + }) + .ok(); +} + +fn run_check(auto_install: bool) -> anyhow::Result<()> { + let current = env!("CARGO_PKG_VERSION").parse()?; + let config = Config { + endpoints: vec![UPDATER_ENDPOINT.parse()?], + pubkey: UPDATER_PUBKEY.into(), + ..Default::default() + }; + + match check_update(current, config)? { + Some(update) => { + tracing::info!("update available: {}", update.version); + if auto_install { + tracing::info!("downloading and installing update…"); + update.download_and_install()?; + tracing::info!("update installed; restart to apply"); + } + } + None => tracing::info!("no update available"), + } + + Ok(()) +} diff --git a/packages/oneclient_core/src/settings/launcher.rs b/packages/oneclient_core/src/settings/launcher.rs index bb850449..9991984f 100644 --- a/packages/oneclient_core/src/settings/launcher.rs +++ b/packages/oneclient_core/src/settings/launcher.rs @@ -25,6 +25,7 @@ pub struct ViewState { pub struct LauncherSettings { pub settings_version: u32, pub log_debug: bool, + pub auto_update: bool, pub enable_gamemode: bool, pub discord_enabled: bool, pub max_concurrent_requests: usize, @@ -55,6 +56,7 @@ impl Default for LauncherSettings { Self { settings_version: 1, log_debug: false, + auto_update: true, discord_enabled: true, enable_gamemode: false, max_concurrent_requests: 25,