mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-12 08:56:47 -05:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07471d404d | ||
|
|
78401059b6 | ||
|
|
84a6a7dcf5 | ||
|
|
4ecd4b7eea |
@@ -0,0 +1,93 @@
|
||||
# This file is machine-generated by `gh actions-lock`.
|
||||
# Do not edit by hand; run `gh actions-lock` to update.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
version: 'v0.0.3'
|
||||
workflows:
|
||||
'.github/workflows/artcraft-linux-publish.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'actions/download-artifact@v8.0.2'
|
||||
- 'actions/setup-node@v7.1.0'
|
||||
- 'actions/upload-artifact@v7.0.2'
|
||||
- 'step-security/dtolnay-rust-toolchain@v1.0.0'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
- 'step-security/rust-cache@v2.9.2'
|
||||
- 'step-security/tauri-action@v1.0.0'
|
||||
- 'storytold/craft-common-actions@main'
|
||||
'.github/workflows/artcraft-macos-publish.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'actions/setup-node@v7.1.0'
|
||||
- 'step-security/dtolnay-rust-toolchain@v1.0.0'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
- 'step-security/tauri-action@v1.0.0'
|
||||
'.github/workflows/artcraft-windows-publish.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'actions/setup-node@v7.1.0'
|
||||
- 'step-security/dtolnay-rust-toolchain@v1.0.0'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
- 'step-security/tauri-action@v1.0.0'
|
||||
'.github/workflows/freebsd.yml': []
|
||||
'.github/workflows/house-rules.yml': []
|
||||
'.github/workflows/lockfile.yml':
|
||||
- 'actions/checkout@v7.0.1'
|
||||
- 'step-security/harden-runner@v2.22.4'
|
||||
'.github/workflows/packaging-lint.yml': []
|
||||
'.github/workflows/sonarcloud.yml': []
|
||||
dependencies:
|
||||
'actions/cache@caa296126883cff596d87d8935842f9db880ef25':
|
||||
ref: 'v5.1.0'
|
||||
commit: 'sha1-caa296126883cff596d87d8935842f9db880ef25'
|
||||
owner_id: 44036562
|
||||
repo_id: 215566462
|
||||
'actions/checkout@v7.0.1':
|
||||
ref: 'v7.0.1'
|
||||
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
|
||||
owner_id: 44036562
|
||||
repo_id: 197814629
|
||||
'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333':
|
||||
ref: 'v8.0.2'
|
||||
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
owner_id: 44036562
|
||||
repo_id: 192626254
|
||||
'actions/download-artifact@v8.0.2':
|
||||
ref: 'v8.0.2'
|
||||
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
owner_id: 44036562
|
||||
repo_id: 192626254
|
||||
'actions/setup-node@v7.1.0':
|
||||
ref: 'v7.1.0'
|
||||
commit: 'sha1-949feb2413d6458794dcd2491c4babbbce0c15c1'
|
||||
owner_id: 44036562
|
||||
repo_id: 189476904
|
||||
'actions/upload-artifact@v7.0.2':
|
||||
ref: 'v7.0.2'
|
||||
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
|
||||
owner_id: 44036562
|
||||
repo_id: 192625955
|
||||
'step-security/dtolnay-rust-toolchain@v1.0.0':
|
||||
ref: 'v1.0.0'
|
||||
commit: 'sha1-8dd607977d06adf4ed49b190381e74c17a9dd25f'
|
||||
owner_id: 88700172
|
||||
repo_id: 1249865941
|
||||
'step-security/harden-runner@v2.22.4':
|
||||
ref: 'v2.22.4'
|
||||
commit: 'sha1-1d88384fd173af6ecb012dfd0264a306b711241a'
|
||||
owner_id: 88700172
|
||||
repo_id: 422287306
|
||||
'step-security/rust-cache@v2.9.2':
|
||||
ref: 'v2.9.2'
|
||||
commit: 'sha1-8871c8ba063416da7c32246932ea6ae08700453e'
|
||||
owner_id: 88700172
|
||||
repo_id: 822636713
|
||||
'step-security/tauri-action@v1.0.0':
|
||||
ref: 'v1.0.0'
|
||||
commit: 'sha1-dc662d498edbd35ec8ed564b45d44e955444d1bc'
|
||||
owner_id: 88700172
|
||||
repo_id: 1283860856
|
||||
'storytold/craft-common-actions@main':
|
||||
ref: 'main'
|
||||
commit: 'sha1-293de72429d3ed5d9fa92aded5e500544c0fa28f'
|
||||
owner_id: 76897702
|
||||
repo_id: 1414429108
|
||||
uses:
|
||||
- 'actions/cache@caa296126883cff596d87d8935842f9db880ef25'
|
||||
- 'actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333'
|
||||
@@ -14,10 +14,10 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: storytold/craft-common-actions/actions/install-apt-packages@main
|
||||
with:
|
||||
packages: rpm jq binutils zsync
|
||||
@@ -39,10 +39,10 @@ jobs:
|
||||
TAURI_APP_PATH: ${{ github.workspace }}/crates/desktop/artcraft
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: storytold/craft-common-actions/actions/install-apt-packages@main
|
||||
with:
|
||||
packages: libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev libclang-dev libasound2-dev libxdo-dev libdbus-1-dev build-essential clang file jq perl cmake nasm rpm zsync gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-libav
|
||||
@@ -71,16 +71,16 @@ jobs:
|
||||
PKG_CONFIG_LIBDIR_riscv64gc_unknown_linux_gnu=/usr/lib/riscv64-linux-gnu/pkgconfig:/usr/share/pkgconfig
|
||||
ENV
|
||||
echo "CMAKE_TOOLCHAIN_FILE_riscv64gc_unknown_linux_gnu=$GITHUB_WORKSPACE/packaging/linux/riscv64.cmake" >> "$GITHUB_ENV"
|
||||
- uses: actions/setup-node@v7
|
||||
- uses: actions/setup-node@v7.1.0
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
- uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
|
||||
- uses: step-security/dtolnay-rust-toolchain@v1.0.0
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.target }}
|
||||
- uses: step-security/rust-cache@8871c8ba063416da7c32246932ea6ae08700453e # v2.9.2
|
||||
- uses: step-security/rust-cache@v2.9.2
|
||||
with:
|
||||
key: tauri-linux-${{ matrix.arch }}
|
||||
cache-on-failure: true
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
working-directory: frontend
|
||||
- run: ./node_modules/.bin/nx run artcraft:build
|
||||
working-directory: frontend
|
||||
- uses: step-security/tauri-action@dc662d498edbd35ec8ed564b45d44e955444d1bc # v1.0.0
|
||||
- uses: step-security/tauri-action@v1.0.0
|
||||
with:
|
||||
projectPath: crates/desktop/artcraft
|
||||
args: --target ${{ matrix.target }} --bundles ${{ matrix.bundles }} --config ${{ github.workspace }}/packaging/linux/tauri.conf.json
|
||||
@@ -97,7 +97,7 @@ jobs:
|
||||
ARCH: ${{ matrix.arch }}
|
||||
TARGET: ${{ matrix.target }}
|
||||
run: packaging/linux/package.sh "$ARCH" "$TARGET"
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v7.0.2
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/release/*
|
||||
@@ -111,11 +111,11 @@ jobs:
|
||||
arch: [x86_64, aarch64]
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/download-artifact@v8.0.2
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/linux
|
||||
@@ -128,7 +128,7 @@ jobs:
|
||||
files=(dist/linux/artcraft-*-linux-"$ARCH".tar.gz)
|
||||
test "${#files[@]}" -eq 1
|
||||
echo "path=${files[0]}" >> "$GITHUB_OUTPUT"
|
||||
- uses: storytold/craft-common-actions/actions/arch-package@9d6790582b4997aa2120f4413cdb55c0b6f77fae # extra-depends support (#161)
|
||||
- uses: storytold/craft-common-actions/actions/arch-package@main # extra-depends support (#161)
|
||||
with:
|
||||
tarball: ${{ steps.tarball.outputs.path }}
|
||||
arch: ${{ matrix.arch }}
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
for package in dist/release/*.pkg.tar.zst; do
|
||||
tar --zstd -xOf "$package" .PKGINFO | grep -Fx 'depend = webkit2gtk-4.1'
|
||||
done
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v7.0.2
|
||||
with:
|
||||
name: release-arch-${{ matrix.arch }}
|
||||
path: dist/release/*.pkg.tar.zst
|
||||
@@ -158,11 +158,11 @@ jobs:
|
||||
- {arch: aarch64, runner: ubuntu-24.04-arm}
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/download-artifact@v8
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- uses: actions/download-artifact@v8.0.2
|
||||
with:
|
||||
name: release-linux-${{ matrix.arch }}
|
||||
path: dist/linux
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
run: packaging/linux/flatpak.sh
|
||||
- uses: actions/upload-artifact@v7
|
||||
- uses: actions/upload-artifact@v7.0.2
|
||||
with:
|
||||
name: release-flatpak-${{ matrix.arch }}
|
||||
path: |
|
||||
@@ -194,10 +194,10 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
- name: Read version
|
||||
id: version
|
||||
run: echo "value=$(jq -r .version crates/desktop/artcraft/tauri.conf.json)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
@@ -39,18 +39,18 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v7.1.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
- name: install Rust stable
|
||||
uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
|
||||
uses: step-security/dtolnay-rust-toolchain@v1.0.0
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ matrix.platform == 'macos-latest-xlarge' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
@@ -230,7 +230,7 @@ jobs:
|
||||
|
||||
|
||||
- name: build and publish
|
||||
uses: step-security/tauri-action@dc662d498edbd35ec8ed564b45d44e955444d1bc # v1.0.0
|
||||
uses: step-security/tauri-action@v1.0.0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
|
||||
@@ -34,13 +34,13 @@ jobs:
|
||||
timeout-minutes: 90
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@1d88384fd173af6ecb012dfd0264a306b711241a # v2.22.4
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v7.0.1
|
||||
|
||||
- name: install Rust stable
|
||||
uses: step-security/dtolnay-rust-toolchain@8dd607977d06adf4ed49b190381e74c17a9dd25f # v1.0.0
|
||||
uses: step-security/dtolnay-rust-toolchain@v1.0.0
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
@@ -54,7 +54,7 @@ jobs:
|
||||
shell: cmd
|
||||
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v7
|
||||
uses: actions/setup-node@v7.1.0
|
||||
with:
|
||||
node-version: lts/*
|
||||
|
||||
@@ -107,7 +107,7 @@ jobs:
|
||||
$config | ConvertTo-Json -Depth 10 | Set-Content tauri.conf.json
|
||||
|
||||
- name: build and publish
|
||||
uses: step-security/tauri-action@dc662d498edbd35ec8ed564b45d44e955444d1bc # v1.0.0
|
||||
uses: step-security/tauri-action@v1.0.0
|
||||
env:
|
||||
# NB(bt): This is needed by wreq. Hopefully it doesn't kill Tauri
|
||||
RUSTFLAGS: "-C target-feature=+crt-static"
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Lockfile
|
||||
|
||||
# Fails when .github/workflows/actions.lock is out of sync with the workflows, or when a locked pin
|
||||
# no longer verifies upstream, so a moved tag cannot slip a different commit into a run.
|
||||
# Docs: https://gh.io/actions-lockfile
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
jobs:
|
||||
lockfile:
|
||||
name: Lockfile is in sync
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Harden runner
|
||||
uses: step-security/harden-runner@v2.22.4
|
||||
with:
|
||||
egress-policy: audit
|
||||
- uses: actions/checkout@v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Install gh-actions-lock
|
||||
run: gh extension install github/gh-actions-lock --pin v0.4.1
|
||||
- name: Verify the lockfile
|
||||
run: gh actions-lock --verify
|
||||
Generated
+174
-68
@@ -304,7 +304,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -383,6 +383,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"cookie_store 0.0.1",
|
||||
"cookie_store_wrapper",
|
||||
"craft-update",
|
||||
"directories",
|
||||
"dirs",
|
||||
"discord-rich-presence",
|
||||
@@ -553,28 +554,6 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
|
||||
|
||||
[[package]]
|
||||
name = "ashpd"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
|
||||
dependencies = [
|
||||
"async-fs",
|
||||
"async-net",
|
||||
"enumflags2",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"rand 0.9.2",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
"url",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "assertor"
|
||||
version = "0.0.3"
|
||||
@@ -638,17 +617,6 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-fs"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
|
||||
dependencies = [
|
||||
"async-lock",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "2.6.0"
|
||||
@@ -678,17 +646,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-net"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"blocking",
|
||||
"futures-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-process"
|
||||
version = "2.5.0"
|
||||
@@ -882,6 +839,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -1468,9 +1431,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
@@ -1812,6 +1775,31 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-update"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=2a24c10817173d26d0ea4689acbbade36f91b5dc#2a24c10817173d26d0ea4689acbbade36f91b5dc"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dunce",
|
||||
"flate2",
|
||||
"getrandom 0.3.1",
|
||||
"log",
|
||||
"plist",
|
||||
"rustix 1.1.5",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"tar",
|
||||
"tempfile",
|
||||
"ureq",
|
||||
"verifysign",
|
||||
"winreg 0.55.0",
|
||||
"winsafe",
|
||||
"zip",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc"
|
||||
version = "3.0.1"
|
||||
@@ -1829,9 +1817,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
version = "1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
checksum = "2b929dd2a135771f40afabd1073f85eed59c800b7c61901e5c254270497dad64"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
@@ -2136,7 +2124,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2508,7 +2496,7 @@ dependencies = [
|
||||
"rustc_version",
|
||||
"toml 0.8.23",
|
||||
"vswhom",
|
||||
"winreg",
|
||||
"winreg 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2698,7 +2686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2862,6 +2850,16 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -4768,7 +4766,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5117,7 +5115,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6272,12 +6270,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
|
||||
|
||||
[[package]]
|
||||
name = "polycool"
|
||||
version = "0.4.0"
|
||||
@@ -6988,7 +6980,6 @@ version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"block2 0.6.2",
|
||||
"dispatch2",
|
||||
"glib-sys",
|
||||
@@ -7000,9 +6991,7 @@ dependencies = [
|
||||
"objc2-app-kit 0.3.2",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation 0.3.2",
|
||||
"pollster",
|
||||
"raw-window-handle",
|
||||
"urlencoding",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
@@ -7172,7 +7161,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7192,6 +7181,7 @@ version = "0.23.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -7242,6 +7232,33 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni 0.22.4",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.36",
|
||||
"rustls-native-certs",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.103.9",
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -8703,6 +8720,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tar"
|
||||
version = "0.4.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
|
||||
dependencies = [
|
||||
"filetime",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.12.14"
|
||||
@@ -9087,7 +9114,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix 1.1.5",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9597,7 +9624,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9661,6 +9688,12 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-path"
|
||||
version = "0.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54ec2755506471f54baf5e4bb5499d8e510cc1caf271270e778a736bd2d7336c"
|
||||
|
||||
[[package]]
|
||||
name = "typeid"
|
||||
version = "1.0.2"
|
||||
@@ -9690,7 +9723,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9842,6 +9875,35 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"flate2",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls 0.23.36",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841583fc36089d7d59419568ae157f43a5a03a2e5a81ba87c77d1596ef46e922"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -9931,6 +9993,12 @@ version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
|
||||
|
||||
[[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"
|
||||
@@ -10046,6 +10114,16 @@ dependencies = [
|
||||
"vello_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "verifysign"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6efd25aadc22c756a9012267fae0b130278d335d6ceb512ec02ce82029b5ca9"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -11381,6 +11459,22 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ef0ffc427f045c0cc9ebffd6f4f91153dcd2a1547b5c3c29ee3f541e16e95c6"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
@@ -11943,6 +12037,19 @@ dependencies = [
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "9.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711e8c9646791c49b7eb799a19c4ae70a9b8372ddc6a1fa4778defd1e2aa5d5e"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"flate2",
|
||||
"indexmap 2.13.0",
|
||||
"memchr",
|
||||
"typed-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.21"
|
||||
@@ -12010,7 +12117,6 @@ dependencies = [
|
||||
"endi",
|
||||
"enumflags2",
|
||||
"serde",
|
||||
"url",
|
||||
"winnow 1.0.0",
|
||||
"zcheapstr",
|
||||
"zvariant_derive",
|
||||
|
||||
@@ -22,6 +22,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
tauri-build = { version = "2.6.2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", rev = "2a24c10817173d26d0ea4689acbbade36f91b5dc" }
|
||||
toml.workspace = true
|
||||
browser_emulation.workspace = true
|
||||
cookie_store_wrapper = { workspace = true, features = ["wreq"] }
|
||||
|
||||
@@ -12,4 +12,5 @@ pub mod platform_info_command;
|
||||
pub mod providers;
|
||||
pub mod response;
|
||||
pub mod task_queue;
|
||||
pub mod deprecated;
|
||||
pub mod deprecated;
|
||||
pub mod updates;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
use crate::updates::{UpdateInfo, Updates};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_updates_command(app: AppHandle, updates: State<'_, Arc<Updates>>) -> Result<UpdateInfo, String> {
|
||||
updates.inner().run("The update check stopped unexpectedly", move |state| state.check(&app)).await
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
use crate::updates::{require_confirmation, Updates};
|
||||
use std::sync::Arc;
|
||||
use tauri::{AppHandle, State};
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn install_update_command(app: AppHandle, updates: State<'_, Arc<Updates>>, confirmed: bool) -> Result<(), String> {
|
||||
require_confirmation(confirmed)?;
|
||||
updates.inner().run("The update installer stopped unexpectedly", move |state| state.install(&app)).await
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod check_for_updates_command;
|
||||
pub mod install_update_command;
|
||||
@@ -6,6 +6,11 @@ use crate::core::commands::generate::generate_mesh_command::generate_mesh_comman
|
||||
pub mod core;
|
||||
pub mod services;
|
||||
pub mod version;
|
||||
pub mod updates;
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::core::commands::updates::check_for_updates_command::check_for_updates_command;
|
||||
use crate::core::commands::updates::install_update_command::install_update_command;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
@@ -155,11 +160,20 @@ pub fn run() {
|
||||
println!("Initializing backend runtime...");
|
||||
|
||||
let builder = tauri::Builder::default()
|
||||
.manage(Arc::new(updates::Updates::default()))
|
||||
.on_window_event(|window, event| {
|
||||
if window.try_state::<Arc<updates::Updates>>().is_some_and(|updates| updates.prevents_exit()) {
|
||||
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
}
|
||||
}
|
||||
})
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_upload::init())
|
||||
.setup(move |app| {
|
||||
updates::show_previous_outcome(app.handle());
|
||||
// TODO(bt): This is broken on windows
|
||||
// log_environment_details();
|
||||
|
||||
@@ -225,6 +239,8 @@ pub fn run() {
|
||||
// TODO: Break this out into another module, because RustRover/IntelliJ lags with these macros.
|
||||
// My first attempt at naively doing this didn't work because the macros can't find their codegen'd targets.
|
||||
let builder = builder.invoke_handler(tauri::generate_handler![
|
||||
check_for_updates_command,
|
||||
install_update_command,
|
||||
check_sora_session_command,
|
||||
download_directory_reveal_command,
|
||||
download_media_file_command,
|
||||
@@ -290,6 +306,14 @@ pub fn run() {
|
||||
worldlabs_receive_bearer_command,
|
||||
]);
|
||||
|
||||
builder.run(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while running tauri application");
|
||||
let app = builder.build(tauri::generate_context!("tauri.conf.json"))
|
||||
.expect("error while building tauri application");
|
||||
app.run(|handle, event| {
|
||||
let updates = handle.state::<Arc<updates::Updates>>();
|
||||
match event {
|
||||
tauri::RunEvent::ExitRequested { api, .. } if updates.prevents_exit() => api.prevent_exit(),
|
||||
tauri::RunEvent::Exit => updates.finish(),
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,5 +2,10 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
if let Some(result) = artcraft_app_lib::updates::finish_helper() {
|
||||
let failed = result.is_err();
|
||||
if let Err(error) = result { log::error!("Update failed: {error}"); }
|
||||
std::process::exit(i32::from(failed));
|
||||
}
|
||||
artcraft_app_lib::run();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
//! Manual update checks and verified installation shared with the native Craft apps.
|
||||
use craft_update::pins::{Choice, Pins};
|
||||
use craft_update::self_update::Install;
|
||||
use craft_update::{self_update, pick_asset, App, Http, Os, PackageKind, Paths, Platform, Release, Stop};
|
||||
use enums::tauri::tasks::task_status::TaskStatus;
|
||||
use serde_derive::Serialize;
|
||||
use sqlite_tasks::queries::list_tasks_for_frontend::list_tasks_for_frontend;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tauri::{AppHandle, Manager};
|
||||
use tauri_plugin_dialog::DialogExt;
|
||||
use crate::core::state::task_database::TaskDatabase;
|
||||
use crate::version::ARTCRAFT_VERSION;
|
||||
|
||||
const APP: App = App { bundle_id_override: Some("ai.artcraft.app"), ..App::new("artcraft", "ArtCraft", "Create images and video", "storytold/artcraft") };
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Updates {
|
||||
busy: AtomicBool,
|
||||
installing: AtomicBool,
|
||||
maybe_release: Mutex<Option<Release>>,
|
||||
maybe_restart: Mutex<Option<Restart>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub current_version: String,
|
||||
pub maybe_available_version: Option<String>,
|
||||
pub can_install: bool,
|
||||
pub tasks_running: bool,
|
||||
pub releases_url: String,
|
||||
}
|
||||
|
||||
type Restart = Box<dyn FnOnce() -> Result<(), String> + Send>;
|
||||
|
||||
/// Only effects cross this boundary. Tests exercise the same state transitions without
|
||||
/// networking, changing an installed copy, reading personal preferences, or exiting a process.
|
||||
trait Backend {
|
||||
fn stable_choice(&self) -> Result<(), String>;
|
||||
fn check(&self) -> Result<Option<Release>, String>;
|
||||
fn idle(&self) -> Result<(), String>;
|
||||
fn installation(&self) -> Option<(Install, Platform)>;
|
||||
fn can_replace(&self) -> bool;
|
||||
fn prepare(&self, install: &Install, release: &Release, platform: Platform, before_replace: &dyn Fn() -> Result<(), String>) -> Result<Restart, String>;
|
||||
fn request_exit(&self);
|
||||
}
|
||||
|
||||
struct NativeBackend<'a>(&'a AppHandle);
|
||||
struct Installing<'a>(&'a AtomicBool);
|
||||
|
||||
pub(crate) struct Operation {
|
||||
updates: Arc<Updates>,
|
||||
}
|
||||
|
||||
impl Updates {
|
||||
pub(crate) fn begin(self: &Arc<Self>) -> Result<Operation, String> {
|
||||
self.busy.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire).map_err(|_| "An update operation is already running. Please wait.".to_owned())?;
|
||||
Ok(Operation { updates: Arc::clone(self) })
|
||||
}
|
||||
|
||||
/// Runs `work` off the async runtime while it holds the one-operation guard.
|
||||
pub(crate) async fn run<T: Send + 'static>(self: &Arc<Self>, stopped: &str, work: impl FnOnce(&Updates) -> Result<T, String> + Send + 'static) -> Result<T, String> {
|
||||
let operation = self.begin()?;
|
||||
let state = Arc::clone(self);
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let _operation = operation;
|
||||
work(&state)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("{stopped}: {error}"))?
|
||||
}
|
||||
|
||||
pub(crate) fn check(&self, app: &AppHandle) -> Result<UpdateInfo, String> {
|
||||
self.check_with(&NativeBackend(app))
|
||||
}
|
||||
|
||||
fn check_with(&self, backend: &impl Backend) -> Result<UpdateInfo, String> {
|
||||
backend.stable_choice()?;
|
||||
let maybe_release = backend.check()?;
|
||||
let tasks_running = backend.idle().is_err();
|
||||
let self_updating = maybe_release.as_ref().is_some_and(|release| backend.can_replace() && backend.installation().is_some_and(|(install, platform)| release_fits_copy(release, &install, platform)));
|
||||
let info = update_info(maybe_release.as_ref(), self_updating, tasks_running);
|
||||
*self.maybe_release.lock().map_err(|_| "The update state is unavailable. Restart ArtCraft and try again.")? = maybe_release;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
pub(crate) fn install(&self, app: &AppHandle) -> Result<(), String> {
|
||||
self.install_with(&NativeBackend(app))
|
||||
}
|
||||
|
||||
fn install_with(&self, backend: &impl Backend) -> Result<(), String> {
|
||||
self.installing.store(true, Ordering::Release);
|
||||
let _installing = Installing(&self.installing);
|
||||
backend.idle()?;
|
||||
backend.stable_choice()?;
|
||||
let release = self.maybe_release.lock().map_err(|_| "The update state is unavailable. Restart ArtCraft and try again.")?.clone().ok_or("Check for updates before installing one.")?;
|
||||
let (install, platform) = backend.installation().ok_or("This installation cannot update itself. Open Releases or use its package manager.")?;
|
||||
if !backend.can_replace() || !release_fits_copy(&release, &install, platform) {
|
||||
return Err("Use Releases or your package manager to update this installation.".into());
|
||||
}
|
||||
// Recheck tasks and launcher choices immediately before replacing any application files.
|
||||
let before_replace = || backend.idle().and_then(|()| backend.stable_choice());
|
||||
let restart = backend.prepare(&install, &release, platform, &before_replace)?;
|
||||
*self.maybe_restart.lock().map_err(|_| "The restart state is unavailable. Restart ArtCraft manually.")? = Some(restart);
|
||||
// The helper starts in RunEvent::Exit, after Tauri accepts the normal exit request.
|
||||
backend.request_exit();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Keep the native process and window alive until replacement finishes. A prepared helper
|
||||
/// may request the normal exit itself, so its restart must not be cancelled here.
|
||||
pub fn prevents_exit(&self) -> bool {
|
||||
self.installing.load(Ordering::Acquire) && self.maybe_restart.lock().map_or(true, |pending| pending.is_none())
|
||||
}
|
||||
|
||||
pub fn finish(&self) {
|
||||
match self.maybe_restart.lock() {
|
||||
Ok(mut pending) => {
|
||||
if let Some(restart) = pending.take() {
|
||||
if let Err(error) = restart() {
|
||||
log::error!("Could not start the update helper: {error}");
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => log::error!("Could not read the prepared update helper"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Installing<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.0.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Operation {
|
||||
fn drop(&mut self) {
|
||||
self.updates.busy.store(false, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finish_helper() -> Option<Result<(), String>> {
|
||||
self_update::finish(&APP, &std::env::args_os().skip(1).collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub fn show_previous_outcome(app: &AppHandle) {
|
||||
let Ok(paths) = Paths::for_app(&APP) else {
|
||||
return;
|
||||
};
|
||||
if let Some(message) = outcome_message(self_update::take_outcome(&paths, ARTCRAFT_VERSION)) {
|
||||
app.dialog().message(message).title("ArtCraft update").show(|_| {});
|
||||
}
|
||||
}
|
||||
|
||||
fn outcome_message(outcome: Option<self_update::Outcome>) -> Option<String> {
|
||||
match outcome? {
|
||||
self_update::Outcome::Installed(version) => Some(format!("ArtCraft was updated to {version}.")),
|
||||
self_update::Outcome::Failed(error) => Some(format!("The ArtCraft update failed: {error}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn require_confirmation(confirmed: bool) -> Result<(), String> {
|
||||
if confirmed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Confirm that you saved your work before installing the update.".into())
|
||||
}
|
||||
}
|
||||
|
||||
fn stable_choice(pins: &Pins) -> Result<(), String> {
|
||||
match pins.choice(APP.id) {
|
||||
Choice::Stable => Ok(()),
|
||||
_ => Err("ArtCraft Launcher manages this app's version. Change its version there.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_idle(app: &AppHandle) -> Result<(), String> {
|
||||
let db = app.try_state::<TaskDatabase>().ok_or("ArtCraft is still starting. Try checking again in a moment.")?;
|
||||
let tasks = tauri::async_runtime::block_on(list_tasks_for_frontend(db.get_connection())).map_err(|error| format!("Could not check running tasks: {error}"))?;
|
||||
no_active_tasks(tasks.tasks.iter().map(|task| task.status))
|
||||
}
|
||||
|
||||
fn no_active_tasks(statuses: impl IntoIterator<Item = TaskStatus>) -> Result<(), String> {
|
||||
if statuses.into_iter().any(active_task) {
|
||||
Err("Finish or cancel running tasks before installing an update.".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn active_task(status: TaskStatus) -> bool {
|
||||
matches!(status, TaskStatus::Pending | TaskStatus::Started | TaskStatus::AttemptFailed)
|
||||
}
|
||||
|
||||
impl Backend for NativeBackend<'_> {
|
||||
fn stable_choice(&self) -> Result<(), String> {
|
||||
stable_choice(&Pins::load_default())
|
||||
}
|
||||
|
||||
fn check(&self) -> Result<Option<Release>, String> {
|
||||
let http = Http::new(&format!("ArtCraft/{ARTCRAFT_VERSION}"))?;
|
||||
craft_update::check_for_update(&APP, &http, ARTCRAFT_VERSION)
|
||||
}
|
||||
|
||||
fn idle(&self) -> Result<(), String> {
|
||||
ensure_idle(self.0)
|
||||
}
|
||||
|
||||
fn installation(&self) -> Option<(Install, Platform)> {
|
||||
self_update::current(&APP).zip(Platform::current())
|
||||
}
|
||||
|
||||
fn can_replace(&self) -> bool {
|
||||
self_update::is_self_updating(&APP)
|
||||
}
|
||||
|
||||
fn prepare(&self, install: &Install, release: &Release, platform: Platform, before_replace: &dyn Fn() -> Result<(), String>) -> Result<Restart, String> {
|
||||
let http = Http::new(&format!("ArtCraft/{ARTCRAFT_VERSION}"))?;
|
||||
let paths = Paths::for_app(&APP)?;
|
||||
let restart = self_update::prepare_update(&APP, install, release, platform, &http, &paths, &Stop::default(), &mut |_| {}, before_replace, Some(ARTCRAFT_VERSION))?;
|
||||
Ok(Box::new(move || restart.start()))
|
||||
}
|
||||
|
||||
fn request_exit(&self) {
|
||||
self.0.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_fits_copy(release: &Release, install: &Install, platform: Platform) -> bool {
|
||||
let kind = match install {
|
||||
Install::MacApp(_) => PackageKind::Dmg,
|
||||
Install::Msi(_) => PackageKind::Msi,
|
||||
Install::Portable(_) => PackageKind::PortableZip,
|
||||
Install::AppImage(_) => PackageKind::AppImage,
|
||||
Install::Package(kind, _) => *kind,
|
||||
Install::Flatpak => PackageKind::Flatpak,
|
||||
// ArtCraft also ships NSIS. It must not be unpacked over as a loose portable copy;
|
||||
// its registered installer remains its update path until the shared helper supports it.
|
||||
Install::Standalone(_) if platform.os == Os::Windows => return false,
|
||||
Install::Standalone(_) => PackageKind::TarGz,
|
||||
};
|
||||
pick_asset(release, &[APP.id], platform, &[kind]).is_some()
|
||||
}
|
||||
|
||||
fn update_info(maybe_release: Option<&Release>, self_updating: bool, tasks_running: bool) -> UpdateInfo {
|
||||
UpdateInfo { current_version: ARTCRAFT_VERSION.into(), maybe_available_version: maybe_release.map(Release::version_label), can_install: !tasks_running && self_updating && maybe_release.is_some(), tasks_running, releases_url: APP.releases_url() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn package_identity_and_display_version_match_the_release_configuration() {
|
||||
let config: serde_json::Value = serde_json::from_str(include_str!("../tauri.conf.json")).expect("release configuration");
|
||||
assert_eq!(config["version"].as_str(), Some(ARTCRAFT_VERSION));
|
||||
assert_eq!(config["identifier"].as_str(), Some(APP.bundle_id().as_str()));
|
||||
let mac: serde_json::Value = serde_json::from_str(include_str!("../tauri-mac.conf.json")).expect("macOS release configuration");
|
||||
assert_eq!(mac["version"].as_str(), Some(ARTCRAFT_VERSION), "the macOS build must report the same version");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tauri_installers_and_flatpaks_are_offered_only_for_the_matching_copy() {
|
||||
use craft_update::{Arch, Asset};
|
||||
let release = Release { assets: ["ArtCraft_0.42.1_x64_en-US.msi", "ArtCraft_0.42.1_universal.dmg", "artcraft-0.42.1-linux-x86_64.flatpak"].into_iter().map(|name| Asset { name: name.into(), ..Asset::default() }).collect(), ..Release::default() };
|
||||
assert!(release_fits_copy(&release, &Install::Msi("unused.exe".into()), Platform { os: Os::Windows, arch: Arch::X64 }));
|
||||
assert!(release_fits_copy(&release, &Install::MacApp("unused.app".into()), Platform { os: Os::MacOs, arch: Arch::Arm64 }));
|
||||
assert!(release_fits_copy(&release, &Install::Flatpak, Platform { os: Os::Linux, arch: Arch::X64 }));
|
||||
assert!(!release_fits_copy(&release, &Install::Flatpak, Platform { os: Os::Linux, arch: Arch::Arm64 }));
|
||||
assert!(!release_fits_copy(&release, &Install::Standalone("unused.exe".into()), Platform { os: Os::Windows, arch: Arch::X64 }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checks_allow_closing_but_an_unfinished_install_keeps_the_window_alive() {
|
||||
let state = Arc::new(Updates::default());
|
||||
let _check = state.begin().expect("checking");
|
||||
assert!(!state.prevents_exit());
|
||||
state.installing.store(true, Ordering::Release);
|
||||
let installing = Installing(&state.installing);
|
||||
assert!(state.prevents_exit());
|
||||
drop(installing);
|
||||
assert!(!state.prevents_exit());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_and_retrying_tasks_block_an_update_but_completed_tasks_do_not() {
|
||||
for status in [TaskStatus::Pending, TaskStatus::Started, TaskStatus::AttemptFailed] {
|
||||
assert!(active_task(status));
|
||||
}
|
||||
for status in [TaskStatus::CompleteSuccess, TaskStatus::CompleteFailure, TaskStatus::Dead, TaskStatus::CancelledByUser, TaskStatus::CancelledByProvider, TaskStatus::CancelledByUs] {
|
||||
assert!(!active_task(status));
|
||||
}
|
||||
assert!(no_active_tasks([TaskStatus::CompleteSuccess, TaskStatus::Dead]).is_ok());
|
||||
assert!(no_active_tasks([]).is_ok());
|
||||
assert_eq!(no_active_tasks([TaskStatus::CompleteSuccess, TaskStatus::Started]), Err("Finish or cancel running tasks before installing an update.".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_stable_track_lets_artcraft_update_itself() {
|
||||
use craft_update::pins::VersionTrack;
|
||||
let mut pins = Pins::default();
|
||||
assert!(stable_choice(&pins).is_ok());
|
||||
pins.set(APP.id, &VersionTrack::Pinned("0.41.0".into()));
|
||||
assert!(stable_choice(&pins).unwrap_err().contains("ArtCraft Launcher manages this app's version"));
|
||||
pins.set(APP.id, &VersionTrack::Nightly);
|
||||
assert!(stable_choice(&pins).is_err());
|
||||
pins.set(APP.id, &VersionTrack::Stable);
|
||||
assert!(stable_choice(&pins).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_previous_update_outcome_is_reported_once_with_its_version_or_error() {
|
||||
let installed = craft_update::parse_version("0.42.1").expect("version");
|
||||
assert_eq!(outcome_message(Some(self_update::Outcome::Installed(installed))).as_deref(), Some("ArtCraft was updated to 0.42.1."));
|
||||
assert_eq!(outcome_message(Some(self_update::Outcome::Failed("disk full".into()))).as_deref(), Some("The ArtCraft update failed: disk full"));
|
||||
assert_eq!(outcome_message(None), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installing_requires_confirmation_and_an_ordinary_start_is_not_the_helper() {
|
||||
assert!(require_confirmation(false).unwrap_err().contains("saved your work"));
|
||||
assert!(require_confirmation(true).is_ok());
|
||||
// The test harness's own arguments never carry the helper flag.
|
||||
assert!(finish_helper().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_run_one_operation_at_a_time_and_release_it_after_failures() {
|
||||
let state = Arc::new(Updates::default());
|
||||
let value = tauri::async_runtime::block_on(state.run("stopped", |_| Ok(7)));
|
||||
assert_eq!(value, Ok(7));
|
||||
let failed: Result<(), String> = tauri::async_runtime::block_on(state.run("stopped", |_| Err("offline".into())));
|
||||
assert_eq!(failed, Err("offline".into()));
|
||||
let held = state.begin().expect("guard released after a failure");
|
||||
let busy = tauri::async_runtime::block_on(state.run("stopped", |_| Ok(())));
|
||||
assert!(busy.unwrap_err().contains("already running"));
|
||||
drop(held);
|
||||
let panicked: Result<(), String> = tauri::async_runtime::block_on(state.run("The update check stopped unexpectedly", |_| panic!("worker crashed")));
|
||||
assert!(panicked.unwrap_err().starts_with("The update check stopped unexpectedly:"));
|
||||
assert!(state.begin().is_ok(), "a crashed worker still releases the guard");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_replaceable_idle_copy_can_offer_installation() {
|
||||
let release = Release { tag: "artcraft-v99.0.0".into(), ..Release::default() };
|
||||
assert!(!update_info(Some(&release), false, false).can_install);
|
||||
assert!(!update_info(Some(&release), true, true).can_install);
|
||||
assert!(!update_info(None, true, false).can_install);
|
||||
assert_eq!(update_info(Some(&release), false, false).maybe_available_version.as_deref(), Some("artcraft-v99.0.0"));
|
||||
assert_eq!(APP.bundle_id(), "ai.artcraft.app");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operations_are_serialized_and_errors_release_the_guard() {
|
||||
let state = Arc::new(Updates::default());
|
||||
let first = state.begin().expect("first check");
|
||||
assert!(state.begin().is_err());
|
||||
drop(first);
|
||||
assert!(state.begin().is_ok());
|
||||
assert!(state.maybe_release.lock().expect("state").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_manual_check_offers_a_matching_idle_install_and_defers_its_helper_until_exit() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
let info = state.check_with(&backend).expect("checked");
|
||||
assert!(info.can_install);
|
||||
state.install_with(&backend).expect("prepared");
|
||||
assert!(backend.exit_requested.load(Ordering::Acquire));
|
||||
assert!(!backend.helper_started.load(Ordering::Acquire));
|
||||
assert!(!state.prevents_exit(), "the prepared update may exit normally");
|
||||
state.finish();
|
||||
assert!(backend.helper_started.load(Ordering::Acquire));
|
||||
state.finish();
|
||||
assert_eq!(backend.helper_runs.load(Ordering::Acquire), 1, "exit events cannot run a helper twice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pin_stops_network_checks_and_replacement() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
backend.pinned.store(true, Ordering::Release);
|
||||
assert!(state.check_with(&backend).unwrap_err().contains("pinned"));
|
||||
assert!(!backend.checked.load(Ordering::Acquire));
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("pinned"));
|
||||
assert!(!state.prevents_exit());
|
||||
assert!(!backend.exit_requested.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn running_tasks_allow_a_check_but_block_the_install() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
backend.busy.store(true, Ordering::Release);
|
||||
let info = state.check_with(&backend).expect("checking while busy");
|
||||
assert!(info.tasks_running);
|
||||
assert!(!info.can_install);
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("busy"));
|
||||
assert!(!state.prevents_exit());
|
||||
assert!(!backend.exit_requested.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pin_or_task_started_during_download_stops_replacement_and_exit() {
|
||||
for pin in [false, true] {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
state.check_with(&backend).expect("checked");
|
||||
backend.pin_during_download.store(pin, Ordering::Release);
|
||||
backend.task_during_download.store(!pin, Ordering::Release);
|
||||
assert!(state.install_with(&backend).is_err());
|
||||
assert!(!state.prevents_exit(), "failed installation releases the close guard");
|
||||
assert!(!backend.exit_requested.load(Ordering::Acquire));
|
||||
assert!(state.maybe_restart.lock().expect("restart state").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_install_requires_a_previous_check_and_a_replaceable_matching_copy() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("Check for updates"));
|
||||
state.check_with(&backend).expect("checked");
|
||||
backend.managed.store(true, Ordering::Release);
|
||||
assert!(!state.check_with(&backend).expect("managed check").can_install);
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("package manager"));
|
||||
backend.managed.store(false, Ordering::Release);
|
||||
backend.missing_install.store(true, Ordering::Release);
|
||||
assert!(!state.check_with(&backend).expect("unknown install").can_install);
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("cannot update"));
|
||||
backend.missing_install.store(false, Ordering::Release);
|
||||
backend.wrong_arch.store(true, Ordering::Release);
|
||||
assert!(!state.check_with(&backend).expect("no matching asset").can_install);
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("package manager"));
|
||||
assert!(!backend.exit_requested.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_new_release_clears_a_previous_offer_and_a_failed_check_is_reported() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
state.check_with(&backend).expect("first check");
|
||||
backend.no_release.store(true, Ordering::Release);
|
||||
assert!(!state.check_with(&backend).expect("current version").can_install);
|
||||
assert!(state.maybe_release.lock().expect("release").is_none());
|
||||
backend.network_error.store(true, Ordering::Release);
|
||||
assert!(state.check_with(&backend).unwrap_err().contains("offline"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_failed_download_never_requests_exit_or_keeps_the_close_guard() {
|
||||
let state = Updates::default();
|
||||
let backend = FakeBackend::default();
|
||||
state.check_with(&backend).expect("check");
|
||||
backend.download_error.store(true, Ordering::Release);
|
||||
assert!(state.install_with(&backend).unwrap_err().contains("download"));
|
||||
assert!(!state.prevents_exit());
|
||||
assert!(!backend.exit_requested.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn poisoned_state_returns_an_error_and_refuses_an_unsafe_exit() {
|
||||
let state = Arc::new(Updates::default());
|
||||
let other = Arc::clone(&state);
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _guard = other.maybe_release.lock().unwrap();
|
||||
panic!("simulate a previous worker failure");
|
||||
})
|
||||
.join();
|
||||
let backend = FakeBackend::default();
|
||||
assert!(state.check_with(&backend).is_err());
|
||||
assert!(state.install_with(&backend).is_err());
|
||||
let other = Arc::clone(&state);
|
||||
let _ = std::thread::spawn(move || {
|
||||
let _guard = other.maybe_restart.lock().unwrap();
|
||||
panic!("simulate a previous helper-state failure");
|
||||
})
|
||||
.join();
|
||||
state.installing.store(true, Ordering::Release);
|
||||
assert!(state.prevents_exit());
|
||||
state.finish();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_supported_install_kind_requires_its_own_asset() {
|
||||
use craft_update::{Arch, Asset};
|
||||
let linux = Platform { os: Os::Linux, arch: Arch::X64 };
|
||||
let windows = Platform { os: Os::Windows, arch: Arch::X64 };
|
||||
let release = Release { assets: ["artcraft-99.0.0-linux-x86_64.AppImage", "artcraft-99.0.0-linux-x86_64.tar.gz", "artcraft-99.0.0-linux-x86_64.deb", "artcraft-99.0.0-windows-x64-portable.zip"].into_iter().map(|name| Asset { name: name.into(), ..Asset::default() }).collect(), ..Release::default() };
|
||||
for install in [Install::AppImage("unused".into()), Install::Standalone("unused".into()), Install::Package(PackageKind::Deb, "unused".into())] {
|
||||
assert!(release_fits_copy(&release, &install, linux));
|
||||
}
|
||||
assert!(release_fits_copy(&release, &Install::Portable("unused".into()), windows));
|
||||
assert!(!release_fits_copy(&release, &Install::Package(PackageKind::Rpm, "unused".into()), linux));
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeBackend {
|
||||
pinned: AtomicBool,
|
||||
busy: AtomicBool,
|
||||
managed: AtomicBool,
|
||||
missing_install: AtomicBool,
|
||||
wrong_arch: AtomicBool,
|
||||
no_release: AtomicBool,
|
||||
network_error: AtomicBool,
|
||||
download_error: AtomicBool,
|
||||
pin_during_download: AtomicBool,
|
||||
task_during_download: AtomicBool,
|
||||
checked: AtomicBool,
|
||||
exit_requested: AtomicBool,
|
||||
helper_started: Arc<AtomicBool>,
|
||||
helper_runs: Arc<std::sync::atomic::AtomicUsize>,
|
||||
}
|
||||
|
||||
impl Backend for FakeBackend {
|
||||
fn stable_choice(&self) -> Result<(), String> {
|
||||
if self.pinned.load(Ordering::Acquire) {
|
||||
Err("pinned".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn check(&self) -> Result<Option<Release>, String> {
|
||||
self.checked.store(true, Ordering::Release);
|
||||
if self.network_error.load(Ordering::Acquire) {
|
||||
return Err("offline".into());
|
||||
}
|
||||
Ok((!self.no_release.load(Ordering::Acquire)).then(|| Release { tag: "artcraft-v99.0.0".into(), assets: vec![craft_update::Asset { name: "artcraft-99.0.0-linux-x86_64.flatpak".into(), ..Default::default() }], ..Default::default() }))
|
||||
}
|
||||
fn idle(&self) -> Result<(), String> {
|
||||
if self.busy.load(Ordering::Acquire) {
|
||||
Err("busy".into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn installation(&self) -> Option<(Install, Platform)> {
|
||||
(!self.missing_install.load(Ordering::Acquire)).then(|| (Install::Flatpak, Platform { os: Os::Linux, arch: if self.wrong_arch.load(Ordering::Acquire) { craft_update::Arch::Arm64 } else { craft_update::Arch::X64 } }))
|
||||
}
|
||||
fn can_replace(&self) -> bool {
|
||||
!self.managed.load(Ordering::Acquire)
|
||||
}
|
||||
fn prepare(&self, _: &Install, _: &Release, _: Platform, before_replace: &dyn Fn() -> Result<(), String>) -> Result<Restart, String> {
|
||||
if self.download_error.load(Ordering::Acquire) {
|
||||
return Err("download failed".into());
|
||||
}
|
||||
if self.pin_during_download.load(Ordering::Acquire) {
|
||||
self.pinned.store(true, Ordering::Release);
|
||||
}
|
||||
if self.task_during_download.load(Ordering::Acquire) {
|
||||
self.busy.store(true, Ordering::Release);
|
||||
}
|
||||
before_replace()?;
|
||||
let started = Arc::clone(&self.helper_started);
|
||||
let runs = Arc::clone(&self.helper_runs);
|
||||
Ok(Box::new(move || {
|
||||
started.store(true, Ordering::Release);
|
||||
runs.fetch_add(1, Ordering::AcqRel);
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
fn request_exit(&self) {
|
||||
self.exit_requested.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
/// The version of the ArtCraft desktop app reported in the UI and via Analytics.
|
||||
/// This should match the CI builds.
|
||||
pub const ARTCRAFT_VERSION: &str = "0.42.0";
|
||||
pub const ARTCRAFT_VERSION: &str = "0.42.1";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.42.0",
|
||||
"version": "0.42.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2.0.0-rc",
|
||||
"productName": "ArtCraft",
|
||||
"version": "0.42.0",
|
||||
"version": "0.42.1",
|
||||
"identifier": "ai.artcraft.app",
|
||||
"build": {
|
||||
"frontendDist": "../../../frontend/apps/artcraft/dist",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import HelpMenuButton from './help-menu';
|
||||
|
||||
@@ -9,4 +9,11 @@ describe('HelpMenuButton', () => {
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('opens the update dialog from Help', async () => {
|
||||
render(<HelpMenuButton />);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Help' }));
|
||||
fireEvent.click(await screen.findByText('Check for Updates…'));
|
||||
expect(await screen.findByRole('dialog', { name: 'Check for Updates' }, { timeout: 5000 })).toBeTruthy();
|
||||
expect(screen.getByText('The web app updates when you reload. Open Releases to get the desktop app.')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ArrowRightIcon, BookIcon, ChevronLeftIcon, CircleHelpIcon, NewspaperIcon } from "lucide-react";
|
||||
import { ArrowRightIcon, BookIcon, ChevronLeftIcon, CircleHelpIcon, NewspaperIcon, RefreshCwIcon } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, YoutubeIcon } from "@storyteller/icons";
|
||||
import { OpenUrl } from "@storyteller/tauri-api";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
@@ -10,6 +10,8 @@ import { PopoverMenu, PopoverItem } from "@storyteller/ui-popover";
|
||||
import { getNewsPosts, markdownToHtml } from "@storyteller/markdown-content";
|
||||
import { clickOnEnterOrSpace } from "@storyteller/common";
|
||||
|
||||
import { UpdateDialog } from "./update-dialog";
|
||||
|
||||
export type HelpMenuButtonProps = {
|
||||
items?: TutorialItem[];
|
||||
panelTitle?: string;
|
||||
@@ -150,6 +152,7 @@ export function HelpMenuButton({
|
||||
onOpenChange,
|
||||
}: HelpMenuButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [updatesOpen, setUpdatesOpen] = useState(false);
|
||||
const tutorials = useMemo(() => items ?? defaultTutorials, [items]);
|
||||
const view = useTutorialModalStore((s) => s.view);
|
||||
const selected = useTutorialModalStore((s) => s.selected);
|
||||
@@ -180,6 +183,7 @@ export function HelpMenuButton({
|
||||
|
||||
// Menu items for the popover
|
||||
const menuItems: PopoverItem[] = [
|
||||
{ label: "Check for Updates…", selected: false, icon: <RefreshCwIcon className="text-base" />, action: "updates" },
|
||||
{
|
||||
label: "Tutorials",
|
||||
selected: false,
|
||||
@@ -214,6 +218,9 @@ export function HelpMenuButton({
|
||||
|
||||
const handleMenuSelect = (item: PopoverItem) => {
|
||||
switch (item.action) {
|
||||
case "updates":
|
||||
setUpdatesOpen(true);
|
||||
break;
|
||||
case "tutorials":
|
||||
handleOpenTutorials();
|
||||
break;
|
||||
@@ -340,6 +347,7 @@ export function HelpMenuButton({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<UpdateDialog isOpen={updatesOpen} onClose={() => setUpdatesOpen(false)} />
|
||||
<PopoverMenu
|
||||
items={menuItems}
|
||||
onSelect={handleMenuSelect}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { StrictMode } from "react";
|
||||
import { act, cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { CheckForUpdates, InstallUpdate, OpenUrl, type UpdateInfo } from "@storyteller/tauri-api";
|
||||
import { UpdateDialog } from "./update-dialog";
|
||||
|
||||
vi.mock("@tauri-apps/api/core", () => ({ isTauri: vi.fn() }));
|
||||
vi.mock("@storyteller/tauri-api", () => ({ CheckForUpdates: vi.fn(), InstallUpdate: vi.fn(), OpenUrl: vi.fn() }));
|
||||
vi.mock("@storyteller/ui-button", () => ({ Button: ({ children, variant: _variant, ...props }: any) => <button {...props}>{children}</button> }));
|
||||
vi.mock("@storyteller/ui-modal", () => ({ Modal: ({ isOpen, title, onClose, showClose, children }: any) => isOpen ? (
|
||||
<section role="dialog" aria-label={title}><button aria-label="Close" disabled={!showClose} onClick={onClose}>×</button>{children}</section>
|
||||
) : null }));
|
||||
|
||||
const INFO: UpdateInfo = { current_version: "0.42.0", maybe_available_version: "0.42.1", can_install: true,
|
||||
tasks_running: false, releases_url: "https://github.com/storytold/artcraft/releases" };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(isTauri).mockReturnValue(true);
|
||||
vi.mocked(CheckForUpdates).mockResolvedValue(INFO);
|
||||
vi.mocked(InstallUpdate).mockResolvedValue(undefined);
|
||||
});
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("manual updates", () => {
|
||||
it("does not check while closed and cancelling never installs", async () => {
|
||||
const close = vi.fn();
|
||||
const view = render(<UpdateDialog isOpen={false} onClose={close} />);
|
||||
expect(CheckForUpdates).not.toHaveBeenCalled();
|
||||
view.rerender(<UpdateDialog isOpen onClose={close} />);
|
||||
await screen.findByText("0.42.1 is available.");
|
||||
expect(screen.getByText("ArtCraft 0.42.0")).toBeTruthy();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
||||
expect(close).toHaveBeenCalledOnce();
|
||||
expect(InstallUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shares StrictMode checks while progress remains visible", async () => {
|
||||
let complete!: (info: UpdateInfo) => void;
|
||||
vi.mocked(CheckForUpdates).mockReturnValue(new Promise((resolve) => { complete = resolve; }));
|
||||
render(<StrictMode><UpdateDialog isOpen onClose={vi.fn()} /></StrictMode>);
|
||||
expect(CheckForUpdates).toHaveBeenCalledOnce();
|
||||
expect(screen.getByRole("status").textContent).toContain("Checking for updates");
|
||||
expect((screen.getByRole("button", { name: "Check again" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
await act(async () => { complete(INFO); });
|
||||
expect(screen.getByText("0.42.1 is available.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("requires an install click and prevents dismissal during installation", async () => {
|
||||
vi.mocked(InstallUpdate).mockReturnValue(new Promise(() => {}));
|
||||
const close = vi.fn();
|
||||
render(<UpdateDialog isOpen onClose={close} />);
|
||||
await screen.findByText("0.42.1 is available.");
|
||||
expect(InstallUpdate).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Update and restart" }));
|
||||
expect(InstallUpdate).toHaveBeenCalledOnce();
|
||||
expect(screen.getByRole("status").textContent).toContain("Downloading and verifying");
|
||||
expect(screen.queryByRole("button", { name: "Cancel" })).toBeNull();
|
||||
expect((screen.getByRole("button", { name: "Close" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((screen.getByRole("button", { name: "Update and restart" }) as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("reports a check failure and allows another manual check", async () => {
|
||||
vi.mocked(CheckForUpdates).mockRejectedValueOnce("Network unavailable");
|
||||
render(<UpdateDialog isOpen onClose={vi.fn()} />);
|
||||
expect((await screen.findByRole("alert")).textContent).toBe("Network unavailable");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Check again" }));
|
||||
await screen.findByText("0.42.1 is available.");
|
||||
expect(CheckForUpdates).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByRole("alert")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores controls after verification fails without closing the app", async () => {
|
||||
vi.mocked(InstallUpdate).mockRejectedValue("Signature verification failed");
|
||||
const close = vi.fn();
|
||||
render(<UpdateDialog isOpen onClose={close} />);
|
||||
await screen.findByText("0.42.1 is available.");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Update and restart" }));
|
||||
expect((await screen.findByRole("alert")).textContent).toBe("Signature verification failed");
|
||||
expect((screen.getByRole("button", { name: "Update and restart" }) as HTMLButtonElement).disabled).toBe(false);
|
||||
expect(screen.getByRole("button", { name: "Cancel" })).toBeTruthy();
|
||||
expect(close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([false, true])("cannot install a managed or busy copy (tasks running: %s)", async (running) => {
|
||||
vi.mocked(CheckForUpdates).mockResolvedValue({ ...INFO, can_install: false, tasks_running: running });
|
||||
render(<UpdateDialog isOpen onClose={vi.fn()} />);
|
||||
await screen.findByText("0.42.1 is available.");
|
||||
const button = screen.getByRole("button", { name: "Update and restart" }) as HTMLButtonElement;
|
||||
expect(button.disabled).toBe(true);
|
||||
fireEvent.click(button);
|
||||
expect(InstallUpdate).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Releases" }));
|
||||
expect(OpenUrl).toHaveBeenCalledWith(INFO.releases_url);
|
||||
});
|
||||
|
||||
it("offers browser users a reload without invoking native commands", () => {
|
||||
vi.mocked(isTauri).mockReturnValue(false);
|
||||
render(<UpdateDialog isOpen onClose={vi.fn()} />);
|
||||
expect(screen.getByRole("status").textContent).toContain("updates when you reload");
|
||||
expect(CheckForUpdates).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("button", { name: "Update and restart" })).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { isTauri } from "@tauri-apps/api/core";
|
||||
import { CheckForUpdates, InstallUpdate, OpenUrl, type UpdateInfo } from "@storyteller/tauri-api";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Modal } from "@storyteller/ui-modal";
|
||||
|
||||
const RELEASES = "https://github.com/storytold/artcraft/releases";
|
||||
|
||||
export function UpdateDialog({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
|
||||
const [info, setInfo] = useState<UpdateInfo | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [checking, setChecking] = useState(false);
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const pendingCheck = useRef<Promise<UpdateInfo> | null>(null);
|
||||
const desktop = isTauri();
|
||||
|
||||
const check = async () => {
|
||||
setChecking(true);
|
||||
setError(null);
|
||||
setInfo(null);
|
||||
try {
|
||||
// StrictMode or a repeated click must share one native background check.
|
||||
const pending = pendingCheck.current ?? CheckForUpdates();
|
||||
pendingCheck.current = pending;
|
||||
setInfo(await pending);
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
} finally {
|
||||
pendingCheck.current = null;
|
||||
setChecking(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && desktop) void check();
|
||||
// A check is manual: reopening this dialog is the only automatic trigger.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, desktop]);
|
||||
|
||||
const install = async () => {
|
||||
if (!info?.can_install || installing) return;
|
||||
setInstalling(true);
|
||||
setError(null);
|
||||
try {
|
||||
await InstallUpdate();
|
||||
// Success closes the application through its normal native exit path.
|
||||
} catch (cause) {
|
||||
setError(cause instanceof Error ? cause.message : String(cause));
|
||||
setInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onClose={() => { if (!installing) onClose(); }}
|
||||
title="Check for Updates"
|
||||
accessibleTitle="Check for Updates"
|
||||
className="max-w-lg"
|
||||
showClose={!installing}
|
||||
closeOnEsc={!installing}
|
||||
closeOnOutsideClick={!installing}
|
||||
>
|
||||
<div className="flex flex-col gap-4 text-base-fg">
|
||||
<p>ArtCraft{info ? ` ${info.current_version}` : ""}</p>
|
||||
<div role="status" aria-live="polite">
|
||||
{!desktop ? "The web app updates when you reload. Open Releases to get the desktop app."
|
||||
: installing ? "Downloading and verifying the update… ArtCraft will restart when it is ready."
|
||||
: checking ? "Checking for updates…"
|
||||
: info?.maybe_available_version ? `${info.maybe_available_version} is available.`
|
||||
: info ? "You're up to date." : null}
|
||||
</div>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{info?.maybe_available_version && (
|
||||
<p className="text-sm text-base-fg/70">
|
||||
{info.tasks_running ? "Finish or cancel running tasks, then check again before updating."
|
||||
: info.can_install ? "Save your work before updating. ArtCraft will close and restart."
|
||||
: "Use Releases or your package manager to update this installation."}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button variant="action" disabled={installing} onClick={() => OpenUrl(info?.releases_url ?? RELEASES)}>Releases</Button>
|
||||
{desktop && <Button variant="action" disabled={checking || installing} onClick={() => void check()}>Check again</Button>}
|
||||
{info?.maybe_available_version && <Button variant="primary" disabled={!info.can_install || checking || installing} onClick={() => void install()}>Update and restart</Button>}
|
||||
{!installing && <Button variant="action" onClick={onClose}>Cancel</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -54,3 +54,5 @@ export * from "./lib/models/modelsStore.js";
|
||||
export * from "./lib/common/commandErrorMessage.js";
|
||||
|
||||
export * from "./lib/generate/GenerateOmni.js";
|
||||
|
||||
export * from "./lib/system/Updates.js";
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
export interface UpdateInfo {
|
||||
current_version: string;
|
||||
maybe_available_version: string | null;
|
||||
can_install: boolean;
|
||||
tasks_running: boolean;
|
||||
releases_url: string;
|
||||
}
|
||||
|
||||
export const CheckForUpdates = (): Promise<UpdateInfo> =>
|
||||
invoke("check_for_updates_command");
|
||||
|
||||
export const InstallUpdate = (): Promise<void> =>
|
||||
invoke("install_update_command", { confirmed: true });
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
# A bundle must reach releases, honor launcher choices, and invoke the verified host updater.
|
||||
set -euo pipefail
|
||||
manifest="$(dirname "$0")/flatpak.yml"
|
||||
for permission in --share=network --talk-name=org.freedesktop.Flatpak --filesystem=xdg-data/ArtCraft/versions:ro; do
|
||||
if ! grep -Fxq -- " - $permission" "$manifest"; then
|
||||
echo "Missing Flatpak update permission: $permission" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
@@ -4,6 +4,10 @@ runtime-version: '51'
|
||||
sdk: org.gnome.Sdk
|
||||
command: artcraft
|
||||
finish-args:
|
||||
# The shared updater inspects and replaces this signed release bundle on the host.
|
||||
- --talk-name=org.freedesktop.Flatpak
|
||||
# Honor launcher-pinned versions without access to other host application data.
|
||||
- --filesystem=xdg-data/ArtCraft/versions:ro
|
||||
- --socket=wayland
|
||||
- --socket=fallback-x11
|
||||
- --socket=pulseaudio
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration fixture: real DEB/RPM archives, with no Rust compilation or signing key.
|
||||
set -euo pipefail
|
||||
bash "$(dirname "$0")/flatpak-update-test.sh"
|
||||
script="$(realpath "$(dirname "$0")/package.sh")"
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
@@ -22,10 +22,18 @@ sonar.exclusions=crates/vendor/**,crates/api_clients/grok_consumer_client/src/cr
|
||||
# test/app/Classes/ApiManager/utils/, which call `it` themselves.
|
||||
# S2819 (message origin): a dedicated worker's `message` events come only from the page that
|
||||
# started it; they carry no origin to check.
|
||||
sonar.issue.ignore.multicriteria=deterministic_rng,api_test_helpers,worker_messages
|
||||
sonar.issue.ignore.multicriteria=deterministic_rng,api_test_helpers,worker_messages,gha-lockfile
|
||||
sonar.issue.ignore.multicriteria.deterministic_rng.ruleKey=rust:S4347
|
||||
sonar.issue.ignore.multicriteria.deterministic_rng.resourceKey=crates/schema/public/tokens/src/deterministic_rng.rs
|
||||
sonar.issue.ignore.multicriteria.api_test_helpers.ruleKey=typescript:S2187
|
||||
sonar.issue.ignore.multicriteria.api_test_helpers.resourceKey=frontend/apps/artcraft/test/app/Classes/ApiManager/**
|
||||
sonar.issue.ignore.multicriteria.worker_messages.ruleKey=typescript:S2819
|
||||
sonar.issue.ignore.multicriteria.worker_messages.resourceKey=**/*.worker.ts
|
||||
|
||||
# githubactions:S7637 asks for every action to be pinned to a commit SHA. A workflow covered by
|
||||
# .github/workflows/actions.lock runs the commit the lockfile records, so its tag refs are pinned
|
||||
# and the lockfile covers the actions those actions use too. Sonar does not read lockfiles yet, so
|
||||
# the rule flags the tag refs anyway:
|
||||
# https://community.sonarsource.com/t/githubactions-s7637-false-positives-for-actions-pinned-through-native-actions-lock/190814
|
||||
sonar.issue.ignore.multicriteria.gha-lockfile.ruleKey=githubactions:S7637
|
||||
sonar.issue.ignore.multicriteria.gha-lockfile.resourceKey=.github/workflows/**
|
||||
|
||||
Reference in New Issue
Block a user