diff --git a/.github/scripts/create-desktop-update-manifest.mjs b/.github/scripts/create-desktop-update-manifest.mjs new file mode 100755 index 00000000000..dd129e82e11 --- /dev/null +++ b/.github/scripts/create-desktop-update-manifest.mjs @@ -0,0 +1,62 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; + +const options = parseArguments(process.argv.slice(2)); +const assets = fs.readdirSync(options.assets).sort(); +const platforms = {}; +const platformArtifacts = [ + [ + 'darwin-aarch64', + selectArtifact(assets, /-aarch64-apple-darwin\.app\.tar\.gz$/i, 'darwin-aarch64'), + ], + [ + 'darwin-x86_64', + selectArtifact(assets, /-x86_64-apple-darwin\.app\.tar\.gz$/i, 'darwin-x86_64'), + ], + ['windows-x86_64', selectArtifact(assets, /-setup\.exe$/i, 'windows-x86_64')], + ['linux-x86_64', selectArtifact(assets, /\.AppImage$/i, 'linux-x86_64')], +]; + +for (const [platform, artifact] of platformArtifacts) { + const signatureFile = `${artifact}.sig`; + if (!assets.includes(signatureFile)) { + throw new Error(`Missing updater signature for ${artifact}`); + } + platforms[platform] = { + signature: fs.readFileSync(path.join(options.assets, signatureFile), 'utf8').trim(), + url: `https://github.com/${options.repository}/releases/download/${options.tag}/${encodeURIComponent(artifact)}`, + }; +} + +const manifest = { + version: options.version, + pub_date: new Date().toISOString(), + platforms, +}; +fs.writeFileSync(options.output, `${JSON.stringify(manifest, null, 2)}\n`); + +function selectArtifact(assets, pattern, platform) { + const matches = assets.filter((asset) => pattern.test(asset)); + if (matches.length !== 1) { + throw new Error( + `Expected one updater artifact for ${platform}, found ${matches.length}: ${matches.join(', ')}`, + ); + } + return matches[0]; +} + +function parseArguments(args) { + const values = {}; + for (let index = 0; index < args.length; index += 2) { + const name = args[index]?.replace(/^--/, ''); + const value = args[index + 1]; + if (!name || value === undefined) throw new Error('Invalid arguments.'); + values[name] = value; + } + for (const required of ['assets', 'repository', 'tag', 'version', 'output']) { + if (!values[required]) throw new Error(`Missing --${required}`); + } + return values; +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d460def921..2ef13a164c8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -828,3 +828,79 @@ jobs: - name: 'Run CLI Integration Tests' run: |- npm run test:integration:cli:sandbox:none + + # + # Desktop Shell: compile + test the Tauri crate in PR CI. + # + # The desktop-release workflow (workflow_dispatch only) is otherwise the sole + # place this crate is built, so a compile error can land on a PR and stay + # invisible until release time. This job compiles the crate and runs its + # release-config tests on every PR that touches the shell. It does not need + # the bundled runtime, so it is cheap. `cargo test` builds the crate and thus + # catches compile failures (e.g. a moved-value error); fmt/clippy are not run + # here because the release pipeline does not gate on them either. + desktop_shell: + name: 'Desktop Shell (ubuntu-22.04)' + needs: 'classify_pr' + if: "${{ !cancelled() && github.event_name != 'push' && needs.classify_pr.outputs.skip_ci != 'true' }}" + runs-on: 'ubuntu-22.04' + timeout-minutes: 45 + permissions: + contents: 'read' + steps: + - name: 'Checkout' + uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + with: + ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}" + fetch-depth: 1 + + # Fail open: any uncertainty (non-PR event, fetch failure) runs the job. + - name: 'Detect desktop-shell changes' + id: 'filter' + env: + EVENT_NAME: '${{ github.event_name }}' + BASE_REF: '${{ github.base_ref }}' + run: |- + changed=true + if [[ "${EVENT_NAME}" == "pull_request" && -n "${BASE_REF}" ]]; then + if git fetch --depth=1 origin "${BASE_REF}" >/dev/null 2>&1; then + if git diff --name-only FETCH_HEAD HEAD | grep -Eq '^(packages/desktop-shell/|\.github/scripts/create-desktop-update-manifest\.mjs|\.github/workflows/ci\.yml)'; then + changed=true + else + changed=false + fi + fi + fi + echo "changed=${changed}" >> "${GITHUB_OUTPUT}" + echo "desktop-shell changed: ${changed}" + + # cargo test links the Tauri/wry webview, so the WebKit/GTK dev headers + # must be present (mirrors the Linux build job in desktop-release.yml). + - name: 'Install Linux dependencies' + if: "${{ steps.filter.outputs.changed == 'true' }}" + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils + + - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 + if: "${{ steps.filter.outputs.changed == 'true' }}" + with: + node-version: '22.x' + + - uses: 'dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4' # stable + if: "${{ steps.filter.outputs.changed == 'true' }}" + + - uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2 + if: "${{ steps.filter.outputs.changed == 'true' }}" + with: + workspaces: 'packages/desktop-shell/src-tauri -> target' + + - name: 'Compile and test the desktop crate' + if: "${{ steps.filter.outputs.changed == 'true' }}" + working-directory: 'packages/desktop-shell' + run: 'cargo test --manifest-path src-tauri/Cargo.toml' + + - name: 'Run desktop release tests' + if: "${{ steps.filter.outputs.changed == 'true' }}" + working-directory: 'packages/desktop-shell' + run: 'node scripts/test-release.js' diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index 6c6efd2798d..d2040e0e12e 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -6,33 +6,21 @@ on: workflow_dispatch: inputs: version: - description: 'Desktop app version to release, for example 0.0.2 or v0.0.2' + description: 'Desktop version, for example 0.1.0 or v0.1.0.' required: true type: 'string' - release_name: - description: 'Release title. Defaults to the tag.' - required: false - type: 'string' - qwen_code_source: - description: 'Qwen Code runtime source to vendor into the desktop app.' - required: true - default: 'source_branch' - type: 'choice' - options: - - 'npm_latest' - - 'source_branch' qwen_code_ref: - description: 'Current repository branch, tag, or commit when qwen_code_source is source_branch.' - required: false + description: 'Qwen Code branch, tag, or commit to bundle.' + required: true default: 'main' type: 'string' dry_run: - description: 'Build installers only. Do not create or update a GitHub Release.' + description: 'Build unsigned installers without publishing.' required: true default: true type: 'boolean' draft: - description: 'Create a draft release.' + description: 'Create a draft GitHub release.' required: true default: true type: 'boolean' @@ -55,685 +43,422 @@ concurrency: cancel-in-progress: false env: - BUN_VERSION: '1.3.9' - CRAFT_BRAND: 'qwen-code' - DESKTOP_UPDATE_FEED_TAG: 'desktop-latest' + NODE_VERSION: '22.20.0' + DESKTOP_FEED_TAG: 'desktop-latest' jobs: - release_metadata: - name: 'Prepare Release Source' + prepare: + name: 'Prepare release metadata' runs-on: 'ubuntu-latest' timeout-minutes: 10 - permissions: - contents: 'write' outputs: - qwen_code_ref: '${{ steps.qwen-code-ref.outputs.ref }}' - qwen_code_sha: '${{ steps.qwen-code-ref.outputs.sha }}' - release_branch: '${{ steps.release-branch.outputs.branch }}' - release_ref: '${{ steps.release-branch.outputs.ref }}' - tag: '${{ steps.release-tag.outputs.tag }}' - version: '${{ steps.release-version.outputs.version }}' - + qwen_code_sha: '${{ steps.source.outputs.sha }}' + tag: '${{ steps.version.outputs.tag }}' + version: '${{ steps.version.outputs.version }}' steps: - - name: 'Check out source' - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 with: fetch-depth: 0 - - name: 'Set up Node' - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 - with: - node-version-file: '.nvmrc' - - - name: 'Set up Bun' - uses: 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' # v2 - with: - bun-version: '${{ env.BUN_VERSION }}' - - - name: 'Install dependencies' - working-directory: 'packages/desktop' - run: 'bun install --frozen-lockfile' - - - name: 'Configure Git user' - run: | - git config user.name "github-actions[bot]" - git config user.email "github-actions[bot]@users.noreply.github.com" - - - name: 'Require main for publishing' - if: '${{ inputs.dry_run == false }}' + - name: 'Resolve version' + id: 'version' + shell: 'bash' env: - SOURCE_REF: '${{ github.ref_name }}' + INPUT_VERSION: '${{ inputs.version }}' run: | set -euo pipefail - - if [ "$SOURCE_REF" != "main" ]; then - echo "::error::Desktop releases with dry_run=false must be run from main. Current ref: $SOURCE_REF" + version="${INPUT_VERSION#v}" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([+-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::Desktop version must be valid SemVer: $INPUT_VERSION" exit 1 fi + echo "version=$version" >> "$GITHUB_OUTPUT" + echo "tag=desktop-v$version" >> "$GITHUB_OUTPUT" - - name: 'Resolve Qwen Code source ref' - id: 'qwen-code-ref' + - name: 'Resolve Qwen Code source' + id: 'source' shell: 'bash' env: + INPUT_REF: '${{ inputs.qwen_code_ref }}' IS_DRY_RUN: '${{ inputs.dry_run }}' - QWEN_CODE_REF_INPUT: '${{ inputs.qwen_code_ref }}' - QWEN_CODE_SOURCE_INPUT: '${{ inputs.qwen_code_source }}' run: | set -euo pipefail - - if [ "$QWEN_CODE_SOURCE_INPUT" != "source_branch" ]; then - echo "ref=" >> "$GITHUB_OUTPUT" - echo "sha=" >> "$GITHUB_OUTPUT" - exit 0 - fi - - if [ -z "$QWEN_CODE_REF_INPUT" ]; then - echo "::error::qwen_code_ref is required when qwen_code_source is source_branch." - exit 1 - fi - - if [ "$IS_DRY_RUN" = "false" ] && [[ "$QWEN_CODE_REF_INPUT" == refs/pull/* ]]; then - echo "::error::Published desktop releases cannot vendor refs/pull/*." - exit 1 - fi - - if ! git fetch origin "$QWEN_CODE_REF_INPUT"; then - if ! git fetch origin "refs/heads/$QWEN_CODE_REF_INPUT"; then - git fetch origin "refs/tags/$QWEN_CODE_REF_INPUT" - fi - fi - + git fetch origin "$INPUT_REF" sha="$(git rev-parse FETCH_HEAD)" - - if [ "$IS_DRY_RUN" = "false" ]; then + if [ "$IS_DRY_RUN" = 'false' ]; then + if [ "$GITHUB_REF_NAME" != 'main' ]; then + echo '::error::Published desktop releases must run from main.' + exit 1 + fi git fetch origin main:refs/remotes/origin/main if ! git merge-base --is-ancestor "$sha" refs/remotes/origin/main; then - echo "::error::Published desktop releases can only vendor commits reachable from main." + echo '::error::Published desktop releases may only bundle commits reachable from main.' exit 1 fi fi - - echo "ref=$QWEN_CODE_REF_INPUT" >> "$GITHUB_OUTPUT" echo "sha=$sha" >> "$GITHUB_OUTPUT" - echo "Resolved Qwen Code ref $QWEN_CODE_REF_INPUT to $sha" - - - name: 'Bump desktop version' - working-directory: 'packages/desktop' - env: - INPUT_VERSION: '${{ inputs.version }}' - run: 'bun run bump-desktop-version "$INPUT_VERSION"' - - - name: 'Validate release version' - working-directory: 'packages/desktop' - id: 'release-version' - env: - INPUT_VERSION: '${{ inputs.version }}' - run: 'bun run check-release-version --version "$INPUT_VERSION"' - - - name: 'Prepare desktop release tag' - id: 'release-tag' - env: - RELEASE_TAG: '${{ steps.release-version.outputs.tag }}' - run: 'echo "tag=desktop-${RELEASE_TAG}" >> "$GITHUB_OUTPUT"' - - - name: 'Create release branch' - working-directory: 'packages/desktop' - id: 'release-branch' - env: - IS_DRY_RUN: '${{ inputs.dry_run }}' - RELEASE_TAG: '${{ steps.release-tag.outputs.tag }}' - run: | - set -euo pipefail - - branch="release/${RELEASE_TAG}" - git switch -C "$branch" - git add package.json apps/electron/package.json packages/shared/package.json - - if git diff --staged --quiet; then - echo "No desktop version changes to commit." - else - git commit -m "chore(release): desktop ${RELEASE_TAG}" - fi - - echo "branch=$branch" >> "$GITHUB_OUTPUT" - - if [ "$IS_DRY_RUN" = "false" ]; then - remote_sha="$(git ls-remote --heads origin "$branch" | awk '{print $1}')" - if [ -n "$remote_sha" ]; then - git push --force-with-lease="refs/heads/$branch:$remote_sha" origin "HEAD:refs/heads/$branch" - else - git push origin "HEAD:refs/heads/$branch" - fi - echo "ref=$branch" >> "$GITHUB_OUTPUT" - else - echo "Dry run enabled. Skipping release branch push." - echo "ref=$GITHUB_SHA" >> "$GITHUB_OUTPUT" - fi build: name: 'Build ${{ matrix.name }}' + needs: 'prepare' runs-on: '${{ matrix.os }}' - timeout-minutes: 90 - needs: 'release_metadata' - env: - RELEASE_TAG: '${{ needs.release_metadata.outputs.tag }}' - RELEASE_VERSION: '${{ needs.release_metadata.outputs.version }}' + timeout-minutes: 120 strategy: fail-fast: false matrix: include: - - name: 'macOS' + - name: 'macOS arm64' os: 'macos-latest' - command: 'bun run dist:mac:no-publish' - - name: 'Windows' + rust_target: 'aarch64-apple-darwin' + tauri_args: '--target aarch64-apple-darwin --bundles app,dmg' + - name: 'macOS x64' + os: 'macos-15' + rust_target: 'x86_64-apple-darwin' + tauri_args: '--target x86_64-apple-darwin --bundles app,dmg' + - name: 'Windows x64' os: 'windows-latest' - command: 'bun run dist:win:no-publish' - - name: 'Linux' + rust_target: 'x86_64-pc-windows-msvc' + tauri_args: '--target x86_64-pc-windows-msvc --bundles nsis' + - name: 'Linux x64' os: 'ubuntu-22.04' - command: 'bun run dist:linux:no-publish' - + rust_target: 'x86_64-unknown-linux-gnu' + tauri_args: '--target x86_64-unknown-linux-gnu --bundles appimage,deb' + env: + QWEN_CODE_COMMIT: '${{ needs.prepare.outputs.qwen_code_sha }}' + QWEN_DESKTOP_TARGET: '${{ matrix.rust_target }}' steps: - - name: 'Check out source' - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 - with: - ref: '${{ needs.release_metadata.outputs.release_ref }}' - - - name: 'Set up Node' - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 - with: - node-version-file: '.nvmrc' + - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + - name: 'Configure source directory' + shell: 'bash' + run: 'echo "QWEN_CODE_ROOT=$RUNNER_TEMP/qwen-code-source" >> "$GITHUB_ENV"' - name: 'Check out Qwen Code source' - if: "${{ inputs.qwen_code_source == 'source_branch' }}" shell: 'bash' - env: - QWEN_CODE_REF_INPUT: '${{ needs.release_metadata.outputs.qwen_code_ref }}' - QWEN_CODE_SHA: '${{ needs.release_metadata.outputs.qwen_code_sha }}' - QWEN_CODE_SOURCE_ROOT: '${{ runner.temp }}/qwen-code-source' run: | set -euo pipefail + git init "$QWEN_CODE_ROOT" + git -C "$QWEN_CODE_ROOT" remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" + git -C "$QWEN_CODE_ROOT" fetch --depth=1 origin "$QWEN_CODE_COMMIT" + git -C "$QWEN_CODE_ROOT" checkout --detach "$QWEN_CODE_COMMIT" - if [ -z "$QWEN_CODE_SHA" ]; then - echo "::error::Resolved Qwen Code source SHA is missing." - exit 1 - fi - - rm -rf "$QWEN_CODE_SOURCE_ROOT" - git init "$QWEN_CODE_SOURCE_ROOT" - git -C "$QWEN_CODE_SOURCE_ROOT" remote add origin "${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}.git" - - if ! git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "$QWEN_CODE_SHA"; then - if ! git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "$QWEN_CODE_REF_INPUT"; then - if ! git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "refs/heads/$QWEN_CODE_REF_INPUT"; then - git -C "$QWEN_CODE_SOURCE_ROOT" fetch --depth=1 origin "refs/tags/$QWEN_CODE_REF_INPUT" - fi - fi - fi - - actual_sha="$(git -C "$QWEN_CODE_SOURCE_ROOT" rev-parse FETCH_HEAD)" - if [ "$actual_sha" != "$QWEN_CODE_SHA" ]; then - echo "::error::Qwen Code ref $QWEN_CODE_REF_INPUT resolved to $actual_sha, expected $QWEN_CODE_SHA." - exit 1 - fi + - uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0 + with: + node-version: '${{ env.NODE_VERSION }}' + cache: 'npm' + cache-dependency-path: | + package-lock.json + packages/desktop-shell/package-lock.json - git -C "$QWEN_CODE_SOURCE_ROOT" checkout --detach "$QWEN_CODE_SHA" - git config --global --add safe.directory "$QWEN_CODE_SOURCE_ROOT" + - uses: 'dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4' # stable + with: + targets: '${{ matrix.rust_target }}' - - name: 'Set up Bun' - uses: 'oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6' # v2 + - uses: 'Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae' # v2 with: - bun-version: '${{ env.BUN_VERSION }}' + workspaces: 'packages/desktop-shell/src-tauri -> target' - - name: 'Install Linux packaging dependencies' + - name: 'Install Linux dependencies' if: "runner.os == 'Linux'" run: | sudo apt-get update - sudo apt-get install -y libfuse2 - - - name: 'Install dependencies' - working-directory: 'packages/desktop' - run: 'bun install --frozen-lockfile' + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libatk-bridge2.0-0 at-spi2-core dbus-x11 patchelf libfuse2 xdg-utils xvfb - - name: 'Install Qwen Code source dependencies' - if: "${{ inputs.qwen_code_source == 'source_branch' }}" + - name: 'Install Qwen Code dependencies' working-directory: '${{ runner.temp }}/qwen-code-source' - run: 'npm ci' + env: + QWEN_SKIP_PREPARE: '1' + run: 'npm ci --no-audit --progress=false' - - name: 'Bump desktop version' - working-directory: 'packages/desktop' - run: 'bun run bump-desktop-version "${{ needs.release_metadata.outputs.version }}"' + - name: 'Install desktop dependencies' + working-directory: 'packages/desktop-shell' + run: 'npm ci' - - name: 'Confirm release version' - working-directory: 'packages/desktop' - run: 'bun run check-release-version --version "${{ needs.release_metadata.outputs.version }}"' + - name: 'Set desktop version' + working-directory: 'packages/desktop-shell' + run: 'node scripts/version.js "${{ needs.prepare.outputs.version }}"' - - name: 'Configure Qwen Code runtime source' + - name: 'Require updater signing key for publishing' + if: '${{ inputs.dry_run == false }}' shell: 'bash' env: - QWEN_CODE_REF_INPUT: '${{ needs.release_metadata.outputs.qwen_code_ref }}' - QWEN_CODE_SHA: '${{ needs.release_metadata.outputs.qwen_code_sha }}' - QWEN_CODE_SOURCE_INPUT: '${{ inputs.qwen_code_source }}' - QWEN_CODE_SOURCE_ROOT: '${{ runner.temp }}/qwen-code-source' + TAURI_SIGNING_PRIVATE_KEY: '${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}' run: | set -euo pipefail + if [ -z "$TAURI_SIGNING_PRIVATE_KEY" ]; then + echo '::error::TAURI_SIGNING_PRIVATE_KEY is required for published releases.' + exit 1 + fi - case "$QWEN_CODE_SOURCE_INPUT" in - npm_latest) - echo "QWEN_CODE_VERSION=latest" >> "$GITHUB_ENV" - echo "Using Qwen Code runtime from npm dist-tag: latest" - ;; - source_branch) - if [ -z "$QWEN_CODE_REF_INPUT" ]; then - echo "::error::qwen_code_ref is required when qwen_code_source is source_branch." - exit 1 - fi - echo "QWEN_CODE_ROOT=$QWEN_CODE_SOURCE_ROOT" >> "$GITHUB_ENV" - echo "Using Qwen Code runtime from ${GITHUB_REPOSITORY} ref: $QWEN_CODE_REF_INPUT ($QWEN_CODE_SHA)" - ;; - *) - echo "::error::Unknown qwen_code_source: $QWEN_CODE_SOURCE_INPUT" - exit 1 - ;; - esac - - - name: 'Verify desktop update feed target' - working-directory: 'packages/desktop' + - name: 'Import macOS certificate' + if: "runner.os == 'macOS' && inputs.dry_run == false" shell: 'bash' env: - EXPECTED_UPDATE_URL: 'https://github.com/${{ github.repository }}/releases/download/${{ env.DESKTOP_UPDATE_FEED_TAG }}' + APPLE_CERTIFICATE: '${{ secrets.APPLE_CERTIFICATE }}' + APPLE_CERTIFICATE_PASSWORD: '${{ secrets.APPLE_CERTIFICATE_PASSWORD }}' + KEYCHAIN_PASSWORD: '${{ secrets.APPLE_KEYCHAIN_PASSWORD }}' run: | set -euo pipefail - - bun run electron:builder-config - - actual_update_url="$(node <<'NODE' - const fs = require('node:fs'); - const yaml = require('js-yaml'); - - const config = yaml.load( - fs.readFileSync('apps/electron/electron-builder.generated.yml', 'utf8'), - ); - const publish = config?.publish; - if (!publish || typeof publish.provider !== 'string') { - process.exit(1); - } - - if (publish.provider === 'github') { - if (!publish.owner || !publish.repo) process.exit(1); - console.log(`https://github.com/${publish.owner}/${publish.repo}/releases`); - } else if (publish.provider === 'generic') { - if (!publish.url) process.exit(1); - console.log(publish.url); - } else { - process.exit(1); + for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD KEYCHAIN_PASSWORD; do + if [ -z "${!name}" ]; then echo "::error::$name is required for published macOS releases."; exit 1; fi + done + certificate="$RUNNER_TEMP/qwen-code-desktop.p12" + keychain="$RUNNER_TEMP/qwen-code-desktop.keychain-db" + printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$certificate" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" + security set-keychain-settings -lut 21600 "$keychain" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain" + security import "$certificate" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$keychain" + security list-keychains -d user -s "$keychain" login.keychain-db + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$keychain" + identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | head -n 1)" + if [ -z "$identity" ]; then echo '::error::Developer ID Application identity was not found.'; exit 1; fi + echo "APPLE_SIGNING_IDENTITY=$identity" >> "$GITHUB_ENV" + + - name: 'Configure macOS notarization' + if: "runner.os == 'macOS' && inputs.dry_run == false" + shell: 'bash' + env: + APPLE_API_ISSUER: '${{ secrets.APPLE_API_ISSUER }}' + APPLE_API_KEY: '${{ secrets.APPLE_API_KEY }}' + APPLE_API_KEY_P8: '${{ secrets.APPLE_API_KEY_P8 }}' + run: | + set -euo pipefail + for name in APPLE_API_ISSUER APPLE_API_KEY APPLE_API_KEY_P8; do + if [ -z "${!name}" ]; then echo "::error::$name is required for macOS notarization."; exit 1; fi + done + key_path="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" + printf '%s' "$APPLE_API_KEY_P8" > "$key_path" + { + echo "APPLE_API_KEY_PATH=$key_path" + echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" + echo "APPLE_API_KEY=$APPLE_API_KEY" + } >> "$GITHUB_ENV" + + - name: 'Import Windows certificate' + if: "runner.os == 'Windows' && inputs.dry_run == false" + shell: 'pwsh' + env: + WINDOWS_CERTIFICATE: '${{ secrets.WINDOWS_CERTIFICATE }}' + WINDOWS_CERTIFICATE_PASSWORD: '${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}' + run: | + if (-not $env:WINDOWS_CERTIFICATE -or -not $env:WINDOWS_CERTIFICATE_PASSWORD) { + throw 'WINDOWS_CERTIFICATE and WINDOWS_CERTIFICATE_PASSWORD are required for published Windows releases.' } - NODE - )" - - if [ "$actual_update_url" != "$EXPECTED_UPDATE_URL" ]; then - echo "::error::Desktop update feed points to $actual_update_url, expected $EXPECTED_UPDATE_URL." - exit 1 - fi - - echo "Desktop update feed: ${actual_update_url}" - - - name: 'Configure optional signing secrets' + $path = Join-Path $env:RUNNER_TEMP 'qwen-code-desktop.pfx' + [IO.File]::WriteAllBytes($path, [Convert]::FromBase64String($env:WINDOWS_CERTIFICATE)) + $password = ConvertTo-SecureString $env:WINDOWS_CERTIFICATE_PASSWORD -AsPlainText -Force + $certificate = Import-PfxCertificate -FilePath $path -CertStoreLocation Cert:\CurrentUser\My -Password $password + $windowsConfig = @{ bundle = @{ windows = @{ certificateThumbprint = $certificate.Thumbprint } } } | ConvertTo-Json -Compress -Depth 3 + "WINDOWS_CONFIG=$windowsConfig" | Out-File -FilePath $env:GITHUB_ENV -Append + + - name: 'Prepare bundled runtime' + working-directory: 'packages/desktop-shell' + run: 'npm run build:runtime' + + - name: 'Verify bundled runtime' + working-directory: 'packages/desktop-shell' + run: 'npm run smoke:runtime' + + - name: 'Run desktop tests' + working-directory: 'packages/desktop-shell' + run: 'npm test' + + - name: 'Run desktop release tests' + working-directory: 'packages/desktop-shell' + run: 'npm run test:release' + + - name: 'Build desktop installers' + working-directory: 'packages/desktop-shell' shell: 'bash' env: - IS_DRY_RUN: '${{ inputs.dry_run }}' - APPLE_NOTARY_API_KEY_P8_BASE64_SECRET: '${{ secrets.APPLE_NOTARY_API_KEY_P8_BASE64 }}' - APPLE_NOTARY_KEY_ID_SECRET: '${{ secrets.APPLE_NOTARY_KEY_ID }}' - APPLE_NOTARY_ISSUER_ID_SECRET: '${{ secrets.APPLE_NOTARY_ISSUER_ID }}' - APPLE_TEAM_ID_SECRET: '${{ secrets.APPLE_TEAM_ID }}' - MAC_CSC_KEY_PASSWORD_SECRET: '${{ secrets.MAC_CSC_KEY_PASSWORD }}' - MAC_CSC_LINK_SECRET: '${{ secrets.MAC_CSC_LINK }}' - CSC_KEY_PASSWORD_SECRET: '${{ secrets.CSC_KEY_PASSWORD }}' - CSC_LINK_SECRET: '${{ secrets.CSC_LINK }}' - WIN_CSC_KEY_PASSWORD_SECRET: '${{ secrets.WIN_CSC_KEY_PASSWORD }}' - WIN_CSC_LINK_SECRET: '${{ secrets.WIN_CSC_LINK }}' - SENTRY_ELECTRON_INGEST_URL_SECRET: '${{ secrets.SENTRY_ELECTRON_INGEST_URL }}' + DRY_RUN: '${{ inputs.dry_run }}' + TAURI_SIGNING_PRIVATE_KEY: '${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}' + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}' + WINDOWS_CONFIG: '${{ env.WINDOWS_CONFIG }}' run: | set -euo pipefail + args=( ${{ matrix.tauri_args }} ) + if [ "$DRY_RUN" = 'true' ]; then + args+=(--no-sign) + elif [ "$RUNNER_OS" = 'Windows' ]; then + args+=(--config "$WINDOWS_CONFIG") + fi + npm run tauri -- build "${args[@]}" - append_env() { - local name="$1" - local value="$2" - - if [ -z "$value" ]; then - return - fi + - name: 'Verify macOS signature' + if: "runner.os == 'macOS' && inputs.dry_run == false" + shell: 'bash' + run: | + set -euo pipefail + app="$(find packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app' -print -quit)" + codesign --verify --deep --strict --verbose=2 "$app" + spctl --assess --type execute --verbose=2 "$app" - { - echo "$name<<__${name}__" - printf '%s\n' "$value" - echo "__${name}__" - } >> "$GITHUB_ENV" - } + - name: 'Verify Windows signature' + if: "runner.os == 'Windows' && inputs.dry_run == false" + shell: 'pwsh' + run: | + $installer = Get-ChildItem packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle/nsis/*.exe | Select-Object -First 1 + $signature = Get-AuthenticodeSignature $installer.FullName + if ($signature.Status -ne 'Valid') { throw "Invalid Authenticode signature: $($signature.Status)" } - mac_csc_link="${MAC_CSC_LINK_SECRET:-$CSC_LINK_SECRET}" - mac_csc_key_password="${MAC_CSC_KEY_PASSWORD_SECRET:-$CSC_KEY_PASSWORD_SECRET}" + - name: 'Smoke packaged application' + if: "runner.os == 'macOS'" + working-directory: 'packages/desktop-shell' + shell: 'bash' + run: | + set -euo pipefail + executable="$(find src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos -path '*.app/Contents/MacOS/*' -type f -perm -111 -print -quit)" + npm run smoke:packaged -- "$executable" - allow_unsigned_artifacts() { - if [ "$IS_DRY_RUN" = "true" ]; then - return 0 - fi + - name: 'Smoke packaged application' + if: "runner.os == 'Windows'" + working-directory: 'packages/desktop-shell' + shell: 'pwsh' + run: | + $executable = Get-ChildItem src-tauri/target/${{ matrix.rust_target }}/release/qwen-code-desktop.exe | Select-Object -First 1 + npm run smoke:packaged -- $executable.FullName - return 1 - } + - name: 'Smoke packaged application' + if: "runner.os == 'Linux'" + working-directory: 'packages/desktop-shell' + shell: 'bash' + run: | + set -euo pipefail + xvfb-run -a npm run smoke:packaged -- src-tauri/target/${{ matrix.rust_target }}/release/qwen-code-desktop - if [ "$RUNNER_OS" = "macOS" ]; then - if [ -n "$mac_csc_link" ]; then - if [ -z "$mac_csc_key_password" ]; then - echo "::error::MAC_CSC_LINK/CSC_LINK is configured, but MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD is missing." - exit 1 - fi - - if [ "$IS_DRY_RUN" = "false" ]; then - if [ -z "$APPLE_NOTARY_API_KEY_P8_BASE64_SECRET" ] || [ -z "$APPLE_NOTARY_KEY_ID_SECRET" ] || [ -z "$APPLE_NOTARY_ISSUER_ID_SECRET" ] || [ -z "$APPLE_TEAM_ID_SECRET" ]; then - echo "::error::Published macOS desktop releases require APPLE_NOTARY_API_KEY_P8_BASE64, APPLE_NOTARY_KEY_ID, APPLE_NOTARY_ISSUER_ID, and APPLE_TEAM_ID for notarization." - exit 1 - fi - fi - - # Materialize the App Store Connect API key (.p8) so electron-builder - # (>=24) notarizes via notarytool. It reads APPLE_API_KEY (a path to - # the .p8 file), APPLE_API_KEY_ID, and APPLE_API_ISSUER from the env. - if [ -n "$APPLE_NOTARY_API_KEY_P8_BASE64_SECRET" ] && [ -n "$APPLE_NOTARY_KEY_ID_SECRET" ] && [ -n "$APPLE_NOTARY_ISSUER_ID_SECRET" ]; then - api_key_path="${RUNNER_TEMP}/apple-notary-key.p8" - printf '%s' "$APPLE_NOTARY_API_KEY_P8_BASE64_SECRET" | base64 --decode > "$api_key_path" - append_env "APPLE_API_KEY" "$api_key_path" - append_env "APPLE_API_KEY_ID" "$APPLE_NOTARY_KEY_ID_SECRET" - append_env "APPLE_API_ISSUER" "$APPLE_NOTARY_ISSUER_ID_SECRET" - fi - - append_env "CSC_LINK" "$mac_csc_link" - append_env "CSC_KEY_PASSWORD" "$mac_csc_key_password" - append_env "APPLE_TEAM_ID" "$APPLE_TEAM_ID_SECRET" - echo "CSC_IDENTITY_AUTO_DISCOVERY=true" >> "$GITHUB_ENV" - else - if ! allow_unsigned_artifacts; then - echo "::error::Published macOS desktop releases require MAC_CSC_LINK/CSC_LINK and MAC_CSC_KEY_PASSWORD/CSC_KEY_PASSWORD so auto-update signature validation can pass." - exit 1 - fi - - echo "CSC_IDENTITY_AUTO_DISCOVERY=false" >> "$GITHUB_ENV" - fi - elif [ "$RUNNER_OS" = "Windows" ]; then - if [ -n "$WIN_CSC_LINK_SECRET" ]; then - if [ -z "$WIN_CSC_KEY_PASSWORD_SECRET" ]; then - echo "::error::WIN_CSC_LINK is configured, but WIN_CSC_KEY_PASSWORD is missing." - exit 1 - fi - - append_env "WIN_CSC_LINK" "$WIN_CSC_LINK_SECRET" - append_env "WIN_CSC_KEY_PASSWORD" "$WIN_CSC_KEY_PASSWORD_SECRET" - else - if [ "$IS_DRY_RUN" = "true" ]; then - echo "Windows signing certificate is not configured; Windows dry-run artifacts will be unsigned." - else - echo "::warning::Windows signing certificate is not configured; published Windows desktop releases will be unsigned." - fi - fi - else - if [ "$RUNNER_OS" != "Linux" ] && [ -n "$CSC_LINK_SECRET" ]; then - echo "::warning::CSC_LINK is configured but not used on $RUNNER_OS." + - name: 'Collect artifacts' + shell: 'bash' + run: | + set -euo pipefail + destination="$RUNNER_TEMP/desktop-artifacts" + mkdir -p "$destination" + bundle_root="packages/desktop-shell/src-tauri/target/${{ matrix.rust_target }}/release/bundle" + while IFS= read -r -d '' artifact; do + name="$(basename "$artifact")" + if [ "$RUNNER_OS" = 'macOS' ]; then + extension='' + stem="$name" + case "$name" in + *.app.tar.gz.sig) stem="${name%.app.tar.gz.sig}"; extension='.app.tar.gz.sig' ;; + *.app.tar.gz) stem="${name%.app.tar.gz}"; extension='.app.tar.gz' ;; + *.dmg) stem="${name%.dmg}"; extension='.dmg' ;; + *) continue ;; + esac + name="${stem}-${{ matrix.rust_target }}${extension}" fi + name="${name// /-}" + cp "$artifact" "$destination/$name" + done < <(find "$bundle_root" -type f \( -name '*.dmg' -o -name '*.AppImage' -o -name '*.deb' -o -name '*.exe' -o -name '*.app.tar.gz' -o -name '*.sig' \) -print0) + if [ -z "$(find "$destination" -type f -print -quit)" ]; then + echo '::error::No desktop artifacts were produced.' + exit 1 fi - append_env "SENTRY_ELECTRON_INGEST_URL" "$SENTRY_ELECTRON_INGEST_URL_SECRET" - - - name: 'Build desktop installer' - working-directory: 'packages/desktop' - # Build jobs only produce artifacts. The publish job below owns GitHub - # Release creation/upload so dry-run, draft, prerelease, and replace - # behavior stays centralized. - run: '${{ matrix.command }}' - - - name: 'Upload installer artifacts' + - if: "${{ github.repository == 'QwenLM/qwen-code' }}" uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1 with: - name: 'desktop-${{ matrix.name }}' + name: 'desktop-${{ matrix.rust_target }}' + path: '${{ runner.temp }}/desktop-artifacts/*' if-no-files-found: 'error' retention-days: 14 - path: | - packages/desktop/apps/electron/release/*.AppImage - packages/desktop/apps/electron/release/*.blockmap - packages/desktop/apps/electron/release/*.dmg - packages/desktop/apps/electron/release/*.exe - packages/desktop/apps/electron/release/*.yml - packages/desktop/apps/electron/release/*.zip + - name: 'List artifacts for fork dry run' + if: "${{ github.repository != 'QwenLM/qwen-code' }}" + shell: 'bash' + run: | + set -euo pipefail + find "$RUNNER_TEMP/desktop-artifacts" -maxdepth 1 -type f -print publish: - name: 'Publish GitHub Release' - runs-on: 'ubuntu-latest' - timeout-minutes: 20 + name: 'Publish GitHub release' + if: '${{ inputs.dry_run == false && github.repository == ''QwenLM/qwen-code'' }}' needs: + - 'prepare' - 'build' - - 'release_metadata' - if: '${{ inputs.dry_run == false }}' + runs-on: 'ubuntu-latest' + timeout-minutes: 20 permissions: contents: 'write' - env: - RELEASE_TAG: '${{ needs.release_metadata.outputs.tag }}' - RELEASE_VERSION: '${{ needs.release_metadata.outputs.version }}' - steps: - - name: 'Download installer artifacts' - uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 + - uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3 + + - uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 with: path: 'release-assets' merge-multiple: true - - name: 'Publish release assets' + - name: 'Generate checksums and updater manifest' + shell: 'bash' env: GH_REPO: '${{ github.repository }}' + RELEASE_TAG: '${{ needs.prepare.outputs.tag }}' + RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' + run: | + set -euo pipefail + cd release-assets + # Generate the updater manifest before checksumming so SHA256SUMS.txt + # also covers desktop-latest.json (fetched unauthenticated by clients). + node ../.github/scripts/create-desktop-update-manifest.mjs \ + --assets . \ + --repository "$GH_REPO" \ + --tag "$RELEASE_TAG" \ + --version "$RELEASE_VERSION" \ + --output desktop-latest.json + sha256sum -- * > SHA256SUMS.txt + + - name: 'Create GitHub release' + id: 'release' + env: GH_TOKEN: '${{ github.token }}' + RELEASE_TAG: '${{ needs.prepare.outputs.tag }}' + RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' RELEASE_DRAFT: '${{ inputs.draft }}' - RELEASE_NAME: '${{ inputs.release_name }}' RELEASE_PRERELEASE: '${{ inputs.prerelease }}' - RELEASE_TARGET: '${{ needs.release_metadata.outputs.release_ref }}' - UPDATE_FEED_TAG: '${{ env.DESKTOP_UPDATE_FEED_TAG }}' - UPLOAD_CLOBBER: '${{ inputs.clobber }}' + RELEASE_CLOBBER: '${{ inputs.clobber }}' run: | set -euo pipefail - - if [[ "$RELEASE_TAG" != desktop-v* ]]; then - echo "::error::Desktop releases must use a desktop-v* tag. Got: $RELEASE_TAG" - exit 1 - fi - - assets=() - while IFS= read -r -d '' file; do - assets+=("$file") - done < <(find release-assets -type f -print0 | sort -z) - - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release assets were downloaded." - exit 1 - fi - - printf 'Release assets:\n' - printf ' %s\n' "${assets[@]}" - - title="${RELEASE_NAME:-$RELEASE_TAG}" - + args=("$RELEASE_TAG" release-assets/* --target "$GITHUB_SHA" --title "Qwen Code Desktop v$RELEASE_VERSION" --generate-notes --latest=false) + if [ "$RELEASE_DRAFT" = 'true' ]; then args+=(--draft); fi + if [ "$RELEASE_PRERELEASE" = 'true' ]; then args+=(--prerelease); fi if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - upload_args=("$RELEASE_TAG" "${assets[@]}") - if [ "$UPLOAD_CLOBBER" = "true" ]; then - upload_args+=(--clobber) + if [ "$RELEASE_CLOBBER" != 'true' ]; then + echo "::error::Release $RELEASE_TAG already exists. Re-run with clobber=true to replace its assets." + exit 1 fi - gh release upload "${upload_args[@]}" + gh release upload "$RELEASE_TAG" release-assets/* --clobber + edit_args=() + if [ "$RELEASE_DRAFT" = 'true' ]; then edit_args+=(--draft); else edit_args+=(--draft=false); fi + if [ "$RELEASE_PRERELEASE" = 'true' ]; then edit_args+=(--prerelease); else edit_args+=(--prerelease=false); fi + gh release edit "$RELEASE_TAG" "${edit_args[@]}" + release_url="$(gh release view "$RELEASE_TAG" --json url --jq '.url')" else - previous_tag="$( - gh release list \ - --repo "$GH_REPO" \ - --limit 100 \ - --json tagName,isDraft,isPrerelease \ - --jq '.[] | select(.isDraft == false and .isPrerelease == false and (.tagName | startswith("desktop-v"))) | .tagName' \ - | grep -vxF "$RELEASE_TAG" \ - | head -n 1 \ - || true - )" - - create_args=( - "$RELEASE_TAG" - "${assets[@]}" - --target "$RELEASE_TARGET" - --title "$title" - ) - if [ -n "$previous_tag" ]; then - release_notes="[Full changelog](https://github.com/${GH_REPO}/compare/${previous_tag}...${RELEASE_TAG})" - else - release_notes="Desktop release ${RELEASE_TAG}." - fi - create_args+=(--notes "$release_notes") - if [ "$RELEASE_DRAFT" = "true" ]; then - create_args+=(--draft) - fi - if [ "$RELEASE_PRERELEASE" = "true" ]; then - create_args+=(--prerelease) - fi - create_args+=(--latest=false) - gh release create "${create_args[@]}" + release_url="$(gh release create "${args[@]}")" fi + echo "url=$release_url" >> "$GITHUB_OUTPUT" - if [ "$RELEASE_DRAFT" = "true" ] || [ "$RELEASE_PRERELEASE" = "true" ]; then - echo "Skipping $UPDATE_FEED_TAG update for draft or prerelease desktop release." - else - feed_title="Qwen Code Desktop latest" - feed_notes="Auto-update feed for ${RELEASE_TAG}. See https://github.com/${GH_REPO}/releases/tag/${RELEASE_TAG}." - feed_upload_args=("$UPDATE_FEED_TAG" "${assets[@]}" --clobber) - if gh release view "$UPDATE_FEED_TAG" >/dev/null 2>&1; then - gh release edit "$UPDATE_FEED_TAG" \ - --draft=false \ - --prerelease=false \ - --latest=false \ - --target "$RELEASE_TARGET" \ - --title "$feed_title" \ - --notes "$feed_notes" - gh release upload "${feed_upload_args[@]}" - else - gh release create "$UPDATE_FEED_TAG" "${assets[@]}" \ - --latest=false \ - --target "$RELEASE_TARGET" \ - --title "$feed_title" \ - --notes "$feed_notes" - fi - fi - - sync-version: - name: 'Sync Release Version to Main' - runs-on: 'ubuntu-latest' - timeout-minutes: 10 - needs: - - 'publish' - - 'release_metadata' - if: '${{ inputs.dry_run == false && inputs.draft == false && inputs.prerelease == false }}' - permissions: - contents: 'write' - pull-requests: 'write' - - steps: - - name: 'Require CI bot token' - env: - CI_BOT_PAT_SECRET: '${{ secrets.CI_BOT_PAT }}' - run: | - set -euo pipefail - - if [ -z "$CI_BOT_PAT_SECRET" ]; then - echo "::error::CI_BOT_PAT is required because GITHUB_TOKEN-created PRs do not trigger pull_request workflows." - exit 1 - fi - - - name: 'Create version sync PR' - id: 'version-pr' + - name: 'Update stable updater feed' + if: '${{ inputs.draft == false && inputs.prerelease == false }}' env: - GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' - RELEASE_BRANCH: '${{ needs.release_metadata.outputs.release_branch }}' - RELEASE_TAG: '${{ needs.release_metadata.outputs.tag }}' + GH_TOKEN: '${{ github.token }}' + FEED_TAG: '${{ env.DESKTOP_FEED_TAG }}' run: | set -euo pipefail - - pr_url="$(gh pr list \ - --repo "$GITHUB_REPOSITORY" \ - --head "$RELEASE_BRANCH" \ - --base main \ - --json url \ - --jq '.[0].url')" - - if [ -z "$pr_url" ]; then - pr_url="$(gh pr create \ - --repo "$GITHUB_REPOSITORY" \ - --base main \ - --head "$RELEASE_BRANCH" \ - --label 'skip-changelog' \ - --title "chore(release): desktop ${RELEASE_TAG}" \ - --body "Automated desktop release PR for ${RELEASE_TAG}. Syncs desktop package versions on main.")" + if gh release view "$FEED_TAG" >/dev/null 2>&1; then + gh release upload "$FEED_TAG" release-assets/desktop-latest.json --clobber + else + gh release create "$FEED_TAG" release-assets/desktop-latest.json --title 'Qwen Code Desktop latest' --notes 'Stable desktop updater feed.' --latest=false fi - echo "url=$pr_url" >> "$GITHUB_OUTPUT" - - - name: 'Enable auto-merge' + - name: 'Publish release summary' env: - GH_TOKEN: '${{ secrets.CI_BOT_PAT }}' - PR_URL: '${{ steps.version-pr.outputs.url }}' - RELEASE_TAG: '${{ needs.release_metadata.outputs.tag }}' + RELEASE_TAG: '${{ needs.prepare.outputs.tag }}' + RELEASE_VERSION: '${{ needs.prepare.outputs.version }}' + RELEASE_URL: '${{ steps.release.outputs.url }}' run: | - set -euo pipefail - - # No --delete-branch: main has a merge queue and gh rejects the flag - # when one is enabled (the queue owns the merge; head-branch deletion - # follows the repo's "Automatically delete head branches" setting). - gh pr merge "$PR_URL" \ - --squash \ - --auto \ - --subject "chore(release): desktop ${RELEASE_TAG} [skip ci]" - - dry-run-summary: - name: 'Dry Run Summary' - runs-on: 'ubuntu-latest' - timeout-minutes: 10 - needs: - - 'build' - - 'release_metadata' - if: '${{ inputs.dry_run }}' - env: - RELEASE_TAG: '${{ needs.release_metadata.outputs.tag }}' - RELEASE_VERSION: '${{ needs.release_metadata.outputs.version }}' - - steps: - - name: 'Download installer artifacts' - uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1 - with: - path: 'release-assets' - merge-multiple: true - - - name: 'List release assets' - run: | - set -euo pipefail - - assets=() - while IFS= read -r -d '' file; do - assets+=("$file") - done < <(find release-assets -type f -print0 | sort -z) - - if [ "${#assets[@]}" -eq 0 ]; then - echo "No release assets were downloaded." - exit 1 - fi - { - echo "## Desktop release dry run" + echo '## Desktop release' echo echo "Version: $RELEASE_VERSION" - echo "Release tag: $RELEASE_TAG" - echo - echo "Built ${#assets[@]} asset(s). No GitHub Release was created or updated." - echo - echo "| Asset | Size |" - echo "| --- | ---: |" - for file in "${assets[@]}"; do - size=$(du -h "$file" | cut -f1) - echo "| $(basename "$file") | $size |" - done + echo "Tag: $RELEASE_TAG" + echo "Release: $RELEASE_URL" } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index d6e21616543..cfbc1b2198c 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ node_modules bower_components package-lock.json +!packages/desktop-shell/package-lock.json # Editors .idea diff --git a/docs/design/2026-07-31-desktop-web-shell-release.md b/docs/design/2026-07-31-desktop-web-shell-release.md new file mode 100644 index 00000000000..1c64f63d861 --- /dev/null +++ b/docs/design/2026-07-31-desktop-web-shell-release.md @@ -0,0 +1,164 @@ +# Desktop Web Shell 发布设计 + +## 问题 + +当前桌面 PoC 已证明 Tauri 可以复用 daemon 提供的 Web Shell,而不需要维护第二套 UI。但 PoC 仍缺少公开发布所需的用户流程、故障恢复、签名更新、安全边界和三平台安装产物。 + +本设计把 `packages/desktop-shell` 完善为薄桌面壳:桌面壳只负责生命周期与平台集成,产品功能继续由 `qwen serve` 和 `@qwen-code/web-shell` 提供。 + +## 目标 + +- macOS、Windows、Linux 使用同一套 Web Shell UI。 +- 首次启动允许用户选择工作区,后续启动恢复最近工作区。 +- daemon 启动失败或运行中退出时提供可操作的恢复界面,而不是静默退出。 +- 桌面壳只加载本地 bootstrap 页面与本机随机端口 daemon;外部 URL 始终交给系统浏览器。 +- 发布产物带版本、来源、许可证、校验和和签名更新元数据。 +- 公共 release 在 macOS 完成签名与公证,在 Windows 完成 Authenticode 签名;Linux 生成 AppImage 和 deb。 + +## 非目标 + +- 不新增桌面专属聊天 UI、会话模型或 daemon API。 +- 不把 Web Shell 复制到桌面包中维护。 +- 不实现多窗口、多工作区同时运行或后台常驻。 +- 不承诺 Store 分发;首个公开版本使用 GitHub Releases。 +- 不内置 Git、shell 或其他系统工具。缺失工具继续由现有 Web Shell 能力反馈。 + +## 架构 + +```mermaid +flowchart LR + A[Tauri bootstrap] -->|选择并持久化 workspace| B[Desktop runtime manager] + B -->|spawn process group| C[Bundled Node + qwen serve] + C -->|authenticated loopback URL| D[Existing Web Shell] + A -->|retry / choose workspace / logs| B + B -->|exit event| A + E[GitHub latest.json + installers] -->|signed updater| B +``` + +### 组件职责 + +| 组件 | 职责 | +| --------------- | ------------------------------------------------------------------ | +| bootstrap 页面 | 启动状态、工作区选择、失败恢复、版本与日志入口 | +| Rust 桌面状态 | 设置持久化、窗口状态、runtime 生命周期、单实例、更新状态 | +| bundled runtime | 当前平台 Node.js、Qwen Code bundle、Web Shell 静态资源 | +| 发布 CI | 三平台构建、签名、公证、smoke、校验和、latest.json、GitHub Release | + +## 启动状态机 + +| 状态 | 用户看到的内容 | 可用操作 | +| ----------------- | -------------------------------- | ------------------------------- | +| `starting` | Qwen Code 品牌启动页和当前工作区 | 等待 | +| `needs_workspace` | 首次启动工作区选择 | 选择目录 | +| `ready` | daemon-served Web Shell | 正常使用 | +| `failed` | 精简错误摘要 | 重试、选择其他目录、打开日志 | +| `stopped` | daemon 意外退出提示 | 重启 daemon、选择目录、打开日志 | + +应用先创建 bootstrap 窗口,再异步启动 daemon。daemon 深度健康检查(`/health?deep=true`)通过后,同一个窗口导航到 `http://127.0.0.1:/#token=`。token 只存在于 URL fragment 中,永远不会随请求发往服务端,因此不需要 cookie 握手,也不会进入 access log 或 Referer。这样慢启动和失败路径都有可见 UI。 + +必须使用深度健康检查:serve fast path 在真正的 runtime(含 Web Shell)挂载之前,就会用 bootstrap app 应答浅层 `/health`。此时 `/health?deep=true` 仍返回 `503 {"reason": "bootstrap"}`,因此只有它变为 200 才代表 Web Shell 可用;若用浅层健康检查判定就绪,导航会撞进 deferred runtime 窗口。 + +## 工作区选择与持久化 + +设置文件存储于 Tauri `app_config_dir` 下的 `desktop-state.json`: + +```json +{ + "workspace": "/absolute/path", + "window": { + "width": 1280, + "height": 820, + "x": 120, + "y": 80, + "maximized": false + } +} +``` + +启动优先级: + +1. `QWEN_DESKTOP_WORKSPACE`,用于开发和自动化测试。 +2. 设置文件中的最近工作区。 +3. 首次启动显示目录选择器。 + +只有已存在且为目录的绝对规范路径会传给 daemon。选择新的工作区时先停止当前 process group,再用新目录重新启动。 + +## Runtime 生命周期与恢复 + +- 每次启动生成 256-bit bearer token,通过子进程环境(`QWEN_SERVER_TOKEN`)下发给 daemon,并通过 URL fragment(`/#token=`)交给 Web Shell 前端;前端读取后从 URL 中清除,并以 `Authorization: Bearer` 头调用 API。fragment 不会发送到服务端,因此不需要 cookie。 +- daemon 绑定 `127.0.0.1` 随机端口并启用 `--require-auth`。 +- stdout 和 stderr 同时写入滚动日志,并保留有限启动摘要供 UI 展示。 +- Rust 监视 daemon 进程退出;非应用退出导致的停止会触发 `runtime-stopped` 事件并返回 bootstrap 故障页。 +- 重试始终创建新的 token 和 daemon,不复用已退出进程。 +- 应用退出时终止整个子进程组,避免 orphan daemon。 + +## 窗口与单实例 + +- 主窗口最小尺寸 900 × 600,默认 1280 × 820。 +- 关闭、移动、缩放和最大化状态持久化;恢复时将不可见屏幕外位置回退到居中。 +- 单实例插件必须最先注册。第二次启动只聚焦并恢复主窗口,不再启动 daemon。 + +## 安全边界 + +- bootstrap CSP:`default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'`。 +- Web Shell 仍由 daemon 生成自身 CSP;桌面壳不放宽 daemon 页面策略。 +- 主窗口只允许 bootstrap 自定义协议和选定 daemon 的同源导航。 +- `http`、`https`、`mailto` 外链交给系统浏览器;`file`、`javascript`、自定义协议拒绝。 +- blob 下载仅允许由主 Web Shell 发起,并由原生下载回调选择安全目标路径。 +- Tauri 不暴露文件系统、shell 或 process JavaScript API;bootstrap 只使用显式 `invoke` command。 +- Windows manifest 使用 `asInvoker`、Common Controls v6 和 long-path awareness。 +- macOS hardened runtime 开启,entitlements 只包含运行 JIT WebView 与网络 client/server 所需能力。 + +## 构建元数据与合规 + +`prepare-runtime.js` 生成: + +- `manifest.json`:桌面版本、Qwen Code 版本、Qwen Code commit、Node 版本、target、构建时间。 +- `checksums.json`:所有 bundled runtime 文件的 SHA-256。 +- 根 `LICENSE` 和桌面 `NOTICE`。 +- Node.js `LICENSE`。 + +打包前 smoke 会校验 manifest、关键文件和 checksum。GitHub Release 同时发布每个安装产物的 `SHA256SUMS.txt`。 + +## 更新模型 + +Tauri updater 使用签名更新产物和固定公开 key。应用启动后后台检查一次更新: + +- 无更新:不打扰用户。 +- 检查失败:写日志,不阻塞启动。 +- 有更新:bootstrap/Web Shell 上方显示原生确认对话框;用户确认后下载并安装,然后重启。 + +发布 CI 使用 `TAURI_SIGNING_PRIVATE_KEY` 与 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 生成 updater signatures。`latest.json` 指向同一 GitHub Release 的平台更新包。只有非 draft、非 prerelease 发布会更新固定的 `desktop-latest` feed release。 + +## 平台发布矩阵 + +| 平台 | 架构 | 安装包 | 签名要求 | +| ------- | ---------- | ------------------------------------- | --------------------------------------- | +| macOS | arm64、x64 | `.dmg`、`.app.tar.gz` updater | Developer ID Application + notarization | +| Windows | x64 | NSIS `.exe` updater/installer | Authenticode SHA-256 + timestamp | +| Linux | x64 | `.AppImage` updater/installer、`.deb` | updater minisign;无 OS code-signing | + +Windows WebView2 使用 download bootstrapper;系统离线且缺失 WebView2 时安装失败会明确提示依赖。Linux CI 安装 Tauri WebKit/GTK、AppImage 和 deb 构建依赖。 + +## 发布流程 + +1. 输入 desktop 版本和需要 vendor 的 Qwen Code ref。 +2. 校验 ref 可追溯到允许发布的提交。 +3. 同步 desktop-shell package、Cargo 和 Tauri 版本。版本仅在每次构建时由 CI 瞬时设置,不会提交回仓库;`main` 分支有意保持开发占位版本(`0.0.1`),已发布版本以 git tag 为准。 +4. 每个平台准备 runtime,运行 checksum/runtime smoke 和 Rust 测试。 +5. 构建安装包和 updater artifacts。 +6. 平台 runner 安装并启动 packaged app,等待 daemon/Web Shell ready 证据。 +7. 上传产物;发布 job 生成 `latest.json` 和 `SHA256SUMS.txt`。 +8. 非 draft stable release 更新 `desktop-latest` feed。 + +缺失签名密钥时只允许 `dry_run=true`,公开发布必须 fail closed。 + +## 验证标准 + +- 首次启动能选择目录并进入 Web Shell。 +- 重启恢复工作区和窗口位置。 +- 无效工作区、缺失 runtime、daemon 提前退出均显示恢复页。 +- daemon 运行中被终止后,用户能在原窗口重启。 +- 外链进入系统浏览器,主窗口不离开 daemon origin。 +- 三平台 packaged app smoke 观测到 `/health`、未认证的 Web Shell root 导航返回 200(且不下发任何 cookie)、未携带 token 的 `/capabilities` 返回 401。 +- updater manifest 签名可被客户端验证,版本回退被拒绝。 diff --git a/eslint.config.js b/eslint.config.js index 08f87d9ee30..cda07f41d5e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -34,6 +34,8 @@ export default tseslint.config( 'docs-site/out/**', '.qwen/**', 'packages/desktop/**', + 'packages/desktop-shell/runtime/**', + 'packages/desktop-shell/src-tauri/target/**', 'packages/cua-driver/**', // vendored trycua/cua driver (Rust + scripts); not qwen-code TS 'packages/mobile-mcp/**', // vendored mobile-next/mobile-mcp; has own eslint config ], @@ -351,6 +353,15 @@ export default tseslint.config( }, }, }, + { + files: ['packages/desktop-shell/bootstrap/**/*.js'], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + }, + // ==================== no-console allowlist ==================== // The following files/packages are allowed to use console.* diff --git a/package.json b/package.json index 7904e9958fb..e808c51dd62 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "packages/channels/gitlab", "packages/channels/plugin-example", "integrations/external-context", - "!packages/desktop" + "!packages/desktop", + "!packages/desktop-shell" ], "repository": { "type": "git", diff --git a/packages/cli/src/serve/server.test.ts b/packages/cli/src/serve/server.test.ts index afd8dfac779..e027136d287 100644 --- a/packages/cli/src/serve/server.test.ts +++ b/packages/cli/src/serve/server.test.ts @@ -20135,6 +20135,22 @@ describe('createServeApp', () => { expect(res.status).toBe(401); }); + it('ignores daemon cookies entirely now that fragment auth replaced them', async () => { + const app = createServeApp({ ...baseOpts, token: 'secret' }); + const cookieOnly = await request(app) + .get('/capabilities') + .set('Host', `127.0.0.1:${baseOpts.port}`) + .set('Cookie', 'qwen-daemon-token=secret'); + expect(cookieOnly.status).toBe(401); + + const cookieWithWrongBearer = await request(app) + .get('/capabilities') + .set('Host', `127.0.0.1:${baseOpts.port}`) + .set('Cookie', 'qwen-daemon-token=secret') + .set('Authorization', 'Bearer wrong'); + expect(cookieWithWrongBearer.status).toBe(401); + }); + it('accepts the right token', async () => { const app = createServeApp({ ...baseOpts, token: 'secret' }); const res = await request(app) diff --git a/packages/desktop-shell/.gitignore b/packages/desktop-shell/.gitignore new file mode 100644 index 00000000000..0461bd45096 --- /dev/null +++ b/packages/desktop-shell/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +runtime/* +!runtime/qwen-code/ +runtime/qwen-code/* +!runtime/qwen-code/.gitkeep +src-tauri/target/ diff --git a/packages/desktop-shell/.npmrc b/packages/desktop-shell/.npmrc new file mode 100644 index 00000000000..7068b0104ed --- /dev/null +++ b/packages/desktop-shell/.npmrc @@ -0,0 +1 @@ +workspaces=false diff --git a/packages/desktop-shell/NOTICE b/packages/desktop-shell/NOTICE new file mode 100644 index 00000000000..1d8d5c8c5dc --- /dev/null +++ b/packages/desktop-shell/NOTICE @@ -0,0 +1,4 @@ +Qwen Code Desktop Shell +Copyright 2026 Qwen Team + +This distribution includes Qwen Code and the Node.js runtime. Qwen Code is licensed under the Apache License 2.0. Node.js license notices are included with the bundled runtime. diff --git a/packages/desktop-shell/README.md b/packages/desktop-shell/README.md new file mode 100644 index 00000000000..2d91ab16d0c --- /dev/null +++ b/packages/desktop-shell/README.md @@ -0,0 +1,26 @@ +# Qwen Code desktop shell + +This package is an isolated Tauri 2 shell around the existing Web Shell. It does not contain a second UI. + +## Runtime layout + +`npm run build:runtime` prepares `runtime/qwen-code/` with: + +- the current platform's Node.js runtime, +- the bundled `qwen` CLI, +- the built Web Shell under `lib/web-shell/`. + +The Tauri app starts `qwen serve` on an ephemeral loopback port with a per-launch bearer token, waits for `/health`, and then opens that same daemon-served Web Shell in the native window. + +## Local development + +From this directory: + +```bash +npm install --workspaces=false +npm run build:runtime --workspaces=false +npm test --workspaces=false +npm run dev --workspaces=false +``` + +Use `QWEN_DESKTOP_WORKSPACE=/absolute/path` to choose the initial workspace. Without it, the app shows a workspace picker on first launch. diff --git a/packages/desktop-shell/bootstrap/bootstrap.js b/packages/desktop-shell/bootstrap/bootstrap.js new file mode 100644 index 00000000000..2e10f72ee07 --- /dev/null +++ b/packages/desktop-shell/bootstrap/bootstrap.js @@ -0,0 +1,191 @@ +const tauri = window.__TAURI__; +const invoke = tauri?.core?.invoke; +const listen = tauri?.event?.listen; + +const title = document.querySelector('#title'); +const detail = document.querySelector('#detail'); +const pulse = document.querySelector('#pulse'); +const workspace = document.querySelector('#workspace'); +const error = document.querySelector('#error'); +const choose = document.querySelector('#choose'); +const retry = document.querySelector('#retry'); +const logs = document.querySelector('#logs'); +const update = document.querySelector('#update'); +const version = document.querySelector('#version'); + +let updateVersion; + +function setWorkspace(path) { + workspace.hidden = !path; + workspace.textContent = path || ''; +} + +function setStatus(kind, heading, message, failure = '') { + title.textContent = heading; + detail.textContent = message; + pulse.className = `pulse ${kind === 'starting' ? '' : kind}`; + error.style.display = failure ? 'block' : 'none'; + error.textContent = failure; + retry.hidden = kind !== 'error'; + choose.disabled = kind === 'starting'; +} + +async function chooseWorkspace() { + if (!invoke) return; + setStatus( + 'starting', + 'Opening workspace', + 'Starting the bundled Qwen Code runtime…', + ); + try { + const path = await invoke('choose_workspace'); + if (path) setWorkspace(path); + else setStatus('idle', 'Choose where to work', 'No folder was selected.'); + } catch (failure) { + setStatus( + 'error', + 'Workspace could not start', + 'Review the details or open the desktop log.', + String(failure), + ); + } +} + +async function retryRuntime() { + if (!invoke) return; + setStatus( + 'starting', + 'Restarting Qwen Code', + 'Checking the bundled runtime and workspace…', + ); + try { + await invoke('restart_runtime'); + } catch (failure) { + setStatus( + 'error', + 'Qwen Code could not restart', + 'Review the details or choose another workspace.', + String(failure), + ); + } +} + +async function installUpdate() { + if (!invoke) return; + update.disabled = true; + update.textContent = `Installing ${updateVersion || 'update'}…`; + try { + await invoke('install_update'); + } catch (failure) { + setStatus( + 'error', + 'Update failed', + 'Qwen Code remains usable. Try again or update manually.', + String(failure), + ); + } finally { + update.disabled = false; + update.textContent = 'Install update'; + } +} + +async function openLogs() { + if (!invoke) return; + try { + await invoke('open_logs'); + } catch (failure) { + setStatus( + 'error', + 'Logs could not open', + 'Review the details or try again.', + String(failure), + ); + } +} + +choose.addEventListener('click', chooseWorkspace); +retry.addEventListener('click', retryRuntime); +logs.addEventListener('click', openLogs); +update.addEventListener('click', installUpdate); + +async function initialize() { + if (!invoke || !listen) { + setStatus( + 'error', + 'Desktop bridge unavailable', + 'The packaged desktop bridge did not initialize.', + 'Restart Qwen Code.', + ); + return; + } + + await Promise.all([ + listen('workspace-required', () => { + setStatus( + 'idle', + 'Choose where to work', + 'Qwen Code runs locally and keeps the existing Web Shell as the only interface.', + ); + }), + listen('runtime-starting', ({ payload }) => { + setWorkspace(payload); + setStatus( + 'starting', + 'Starting Qwen Code', + 'Launching the bundled runtime and checking its health…', + ); + }), + listen('runtime-failed', ({ payload }) => { + setStatus( + 'error', + 'Qwen Code could not start', + 'Review the details, open the log, or choose another workspace.', + String(payload), + ); + }), + listen('update-available', ({ payload }) => { + updateVersion = String(payload); + update.hidden = false; + update.textContent = `Install ${updateVersion}`; + }), + ]); + + const state = await invoke('bootstrap_state'); + version.textContent = `Desktop ${state.desktopVersion}`; + setWorkspace(state.workspace); + if (state.status === 'starting') { + setStatus( + 'starting', + 'Starting Qwen Code', + 'Launching the bundled runtime and checking its health…', + ); + } else if (state.status === 'ready') { + setStatus( + 'starting', + 'Loading Qwen Code', + 'Connecting to the local Web Shell…', + ); + } else if (state.error) { + setStatus( + 'error', + 'Qwen Code could not start', + 'Review the details, open the log, or choose another workspace.', + state.error, + ); + } else { + setStatus( + 'idle', + 'Choose where to work', + 'Qwen Code runs locally and keeps the existing Web Shell as the only interface.', + ); + } +} + +initialize().catch((failure) => { + setStatus( + 'error', + 'Desktop initialization failed', + 'Restart Qwen Code or inspect the desktop log.', + String(failure), + ); +}); diff --git a/packages/desktop-shell/bootstrap/index.html b/packages/desktop-shell/bootstrap/index.html new file mode 100644 index 00000000000..c4477e6bc8d --- /dev/null +++ b/packages/desktop-shell/bootstrap/index.html @@ -0,0 +1,271 @@ + + + + + + + Qwen Code + + + +
+
+
+
Q
+
+

Qwen Code

+

Desktop workspace

+
+
+
+
+
+

Choose where to work

+

+ Qwen Code runs locally and keeps the existing Web Shell as the + only interface. +

+ +

+          
+
+
+ + + + +
+
+ DesktopLocal daemon · authenticated loopback +
+
+
+ + + diff --git a/packages/desktop-shell/package-lock.json b/packages/desktop-shell/package-lock.json new file mode 100644 index 00000000000..ccf0961d12f --- /dev/null +++ b/packages/desktop-shell/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "@qwen-code/desktop-shell", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@qwen-code/desktop-shell", + "version": "0.0.1", + "devDependencies": { + "@tauri-apps/cli": "^2.8.5" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.4.tgz", + "integrity": "sha512-R8xGtMpwyetawSqm9kYOuMmEqkhUbvcUy8n0aNXIxollKBLESUu5f4Fx+64hgASYm1H+jSWq6jCW6zqTnH6hqQ==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.4", + "@tauri-apps/cli-darwin-x64": "2.11.4", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.4", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.4", + "@tauri-apps/cli-linux-arm64-musl": "2.11.4", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-gnu": "2.11.4", + "@tauri-apps/cli-linux-x64-musl": "2.11.4", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.4", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.4", + "@tauri-apps/cli-win32-x64-msvc": "2.11.4" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.4.tgz", + "integrity": "sha512-1ryOF3ZhpZ/nemHV5zVwBQBz9jDGKmKPvWPADOhc83ig0P4bMc2iER4NbC6r9sjeIZ6RVQ4g3RZIYvezhcl4TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.4.tgz", + "integrity": "sha512-uFsGQAAfuyz1k/yGLmkWfkBlgKAqZfxqlHmLWx81QU27RJWfmbNHCIq8T8w1e+VClleIuZUjpHWfoE4E3DLo3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.4.tgz", + "integrity": "sha512-IaHZn5CdBL21oUmjiVOS1ctw6Ip1O0pjp70FwOWmYz1myWe0SY96ZIj2FYf7pT0m8bI2h/hrs5ZbEXXh44/MkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.4.tgz", + "integrity": "sha512-N41/ukTRVe6XSuUTESuFdGeOW2i7k62tK+6gHK5Kd5/q5RPvvi19GaWAVPPb9u95HSGmTChSolBfzynUsssFaA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.4.tgz", + "integrity": "sha512-v277UnT/fB64xAfSroL5N3Km3tLmvATWqJJw/wRI+g6o+HkeD0slyE7gOhNs1MbjE41R7bQOTxMVoL3aomUJmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.4.tgz", + "integrity": "sha512-qqgNkQ2u1yZHxjhxsZaxUtRDW8dIqIYm33rx/mzwQv0SfY9x1B+iraj8vWeFiXjjSVVhEMepXSOts1TqPzvXNQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.4.tgz", + "integrity": "sha512-2VRNWl84FOH0m2giiDkO2h0QXlcMJeX+zJDpI5kDIQAx6s+geF3v48F4DXfJez4GS/FdoDGnPnw1C2iYGbQ7bQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.4.tgz", + "integrity": "sha512-o9GyhYor/nc7xarmwDE3ka2szuW3uuZzXjHWh64Q8YX5AtSgxdQkFWzrY4O8KiGtVNvFBI14H3Q49Qj5TOIP/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.4.tgz", + "integrity": "sha512-ld5Ehb598m0VkYyylRPNeCFsBe/km0jxis6KgMpl3IGY6I/i1RwQXO05I1AsXUXO2WC6AvB/Lw4qTf/asiuEiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.4.tgz", + "integrity": "sha512-12Hxi0XX/H5VFxO/bGgHkFWhml9VMgEOu9CidjeCeTNQ1l6fpUlbiGgSP7CLI3PFtW9/FfbeHieZ+kyWK5H7CA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.4.tgz", + "integrity": "sha512-+vDiqBIU5dMISg/wNvX3sF+ZHfgJGJ5T0AcO+EHNXV9GGAG+P5fzodlDXD3QdKCRgZxMoCm5PPvj3BqLNjBthw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/packages/desktop-shell/package.json b/packages/desktop-shell/package.json new file mode 100644 index 00000000000..7b59fe3d0a0 --- /dev/null +++ b/packages/desktop-shell/package.json @@ -0,0 +1,20 @@ +{ + "name": "@qwen-code/desktop-shell", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "tauri": "tauri", + "dev": "tauri dev", + "build": "npm run build:runtime && tauri build", + "build:runtime": "node scripts/prepare-runtime.js", + "smoke:runtime": "node scripts/smoke-runtime.js", + "smoke:packaged": "node scripts/smoke-packaged.js", + "test:release": "node scripts/test-release.js", + "version": "node scripts/version.js", + "test": "cargo test --manifest-path src-tauri/Cargo.toml" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.8.5" + } +} diff --git a/packages/desktop-shell/runtime/qwen-code/.gitkeep b/packages/desktop-shell/runtime/qwen-code/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/desktop-shell/scripts/prepare-runtime.js b/packages/desktop-shell/scripts/prepare-runtime.js new file mode 100755 index 00000000000..7cc00cb2cbe --- /dev/null +++ b/packages/desktop-shell/scripts/prepare-runtime.js @@ -0,0 +1,284 @@ +#!/usr/bin/env node + +import { execFileSync } from 'node:child_process'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pipeline } from 'node:stream/promises'; +import { fileURLToPath } from 'node:url'; + +const packageDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const repoRoot = path.resolve(packageDir, '../..'); +const sourceRoot = process.env.QWEN_CODE_ROOT + ? path.resolve(process.env.QWEN_CODE_ROOT) + : repoRoot; +const runtimeDir = path.join(packageDir, 'runtime'); +const packageRoot = path.join(runtimeDir, 'qwen-code'); +const libDir = path.join(packageRoot, 'lib'); +const nodeDir = path.join(packageRoot, 'node'); +const qwenCodeVersion = JSON.parse( + fs.readFileSync(path.join(sourceRoot, 'package.json'), 'utf8'), +).version; +const desktopVersion = JSON.parse( + fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'), +).version; +const binDir = path.join(packageRoot, 'bin'); + +const target = desktopTarget(); +const skipBuild = process.env.QWEN_DESKTOP_SKIP_BUILD === '1'; + +const npm = process.env.npm_execpath; +if (!npm) throw new Error('npm_execpath is unavailable. Run through npm.'); + +if (!skipBuild) { + execFileSync(process.execPath, [npm, 'run', 'build', '--', '--cli-only'], { + cwd: sourceRoot, + stdio: 'inherit', + }); + execFileSync( + process.execPath, + [npm, 'run', 'build', '--workspace=packages/webui'], + { + cwd: sourceRoot, + stdio: 'inherit', + }, + ); + execFileSync( + process.execPath, + [npm, 'run', 'build', '--workspace=packages/web-shell'], + { + cwd: sourceRoot, + stdio: 'inherit', + }, + ); + execFileSync(process.execPath, [npm, 'run', 'bundle'], { + cwd: sourceRoot, + stdio: 'inherit', + }); + execFileSync(process.execPath, [npm, 'run', 'prepare:package'], { + cwd: sourceRoot, + stdio: 'inherit', + }); +} + +const distDir = path.join(sourceRoot, 'dist'); +for (const required of [ + 'cli.js', + 'cli-entry.js', + 'web-shell/index.html', + 'web-shell/assets', +]) { + const candidate = path.join(distDir, required); + if (!fs.existsSync(candidate)) { + throw new Error(`Missing bundled runtime asset: ${candidate}`); + } +} + +fs.rmSync(runtimeDir, { recursive: true, force: true }); +fs.mkdirSync(libDir, { recursive: true }); +fs.writeFileSync(path.join(packageRoot, '.gitkeep'), ''); +fs.mkdirSync(binDir, { recursive: true }); +copyDirectory(distDir, libDir); +await installNodeRuntime(nodeDir, target); +writeLaunchers(target); +copyRequiredFile( + path.join(sourceRoot, 'LICENSE'), + path.join(packageRoot, 'LICENSE'), +); +copyRequiredFile( + path.join(packageDir, 'NOTICE'), + path.join(packageRoot, 'NOTICE'), +); +const nodeLicense = path.join(nodeDir, 'LICENSE'); +if (!fs.existsSync(nodeLicense)) { + throw new Error(`Bundled Node.js license is missing: ${nodeLicense}`); +} +fs.writeFileSync( + path.join(packageRoot, 'manifest.json'), + `${JSON.stringify( + { + name: '@qwen-code/qwen-code', + desktopVersion, + qwenCodeVersion, + qwenCodeCommit: process.env.QWEN_CODE_COMMIT || gitCommit(sourceRoot), + target, + node: `v${process.versions.node}`, + builtAt: new Date().toISOString(), + }, + null, + 2, + )}\n`, +); +writeChecksums(); +console.log( + `Prepared desktop runtime at ${path.relative(repoRoot, packageRoot)}`, +); + +async function installNodeRuntime(destination, desktopTarget) { + const nvmrc = fs.readFileSync(path.join(repoRoot, '.nvmrc'), 'utf8').trim(); + const nodeVersion = process.versions.node; + if (!nodeVersion.startsWith(`${nvmrc}.`)) { + throw new Error( + `Node ${nodeVersion} does not match .nvmrc major version ${nvmrc}. ` + + 'Run the correct Node or update .nvmrc.', + ); + } + const archiveName = nodeArchiveName(nodeVersion, desktopTarget); + const downloadRoot = `https://nodejs.org/dist/v${nodeVersion}`; + const temporaryRoot = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-desktop-node-'), + ); + try { + const checksumsPath = path.join(temporaryRoot, 'SHASUMS256.txt'); + const archivePath = path.join(temporaryRoot, archiveName); + await download(`${downloadRoot}/SHASUMS256.txt`, checksumsPath); + await download(`${downloadRoot}/${archiveName}`, archivePath); + verifyChecksum( + archivePath, + archiveName, + fs.readFileSync(checksumsPath, 'utf8'), + ); + extractNodeArchive(archivePath, temporaryRoot); + const extractedRoot = path.join( + temporaryRoot, + archiveName.replace(/\.(tar\.gz|tar\.xz|zip)$/, ''), + ); + if (!fs.existsSync(extractedRoot)) { + throw new Error(`Extracted Node runtime is missing: ${extractedRoot}`); + } + copyDirectory(extractedRoot, destination); + } finally { + fs.rmSync(temporaryRoot, { recursive: true, force: true }); + } +} + +function desktopTarget() { + const target = + process.env.QWEN_DESKTOP_TARGET || `${process.platform}-${process.arch}`; + const aliases = { + 'aarch64-apple-darwin': 'darwin-arm64', + 'x86_64-apple-darwin': 'darwin-x64', + 'aarch64-unknown-linux-gnu': 'linux-arm64', + 'x86_64-unknown-linux-gnu': 'linux-x64', + 'x86_64-pc-windows-msvc': 'win32-x64', + }; + const resolved = aliases[target] || target; + if ( + ![ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64', + 'linux-x64', + 'win32-x64', + ].includes(resolved) + ) { + throw new Error(`Unsupported desktop target: ${target}`); + } + return resolved; +} + +function nodeArchiveName(version, desktopTarget) { + const nodeTarget = desktopTarget === 'win32-x64' ? 'win-x64' : desktopTarget; + const extension = desktopTarget.startsWith('darwin-') + ? 'tar.gz' + : desktopTarget.startsWith('linux-') + ? 'tar.xz' + : 'zip'; + return `node-v${version}-${nodeTarget}.${extension}`; +} + +async function download(url, destination) { + const response = await fetch(url, { signal: AbortSignal.timeout(120_000) }); + if (!response.ok || !response.body) { + throw new Error(`Failed to download ${url}: HTTP ${response.status}`); + } + await pipeline(response.body, fs.createWriteStream(destination)); +} + +function verifyChecksum(archivePath, archiveName, checksums) { + const expected = checksums + .split(/\r?\n/) + .map((line) => line.trim().split(/\s+/)) + .find(([, fileName]) => fileName === archiveName)?.[0]; + if (!expected) { + throw new Error(`Node checksums do not list ${archiveName}`); + } + const actual = crypto + .createHash('sha256') + .update(fs.readFileSync(archivePath)) + .digest('hex'); + if (actual !== expected) { + throw new Error(`Node runtime checksum mismatch for ${archiveName}`); + } +} + +function extractNodeArchive(archivePath, destination) { + execFileSync('tar', ['-xf', archivePath, '-C', destination]); +} + +function writeLaunchers(desktopTarget) { + if (desktopTarget.startsWith('win32-')) { + fs.writeFileSync( + path.join(binDir, 'qwen.cmd'), + '@echo off\r\nsetlocal\r\nset "ROOT=%~dp0.."\r\n"%ROOT%\\node\\node.exe" "%ROOT%\\lib\\cli-entry.js" %*\r\nexit /b %ERRORLEVEL%\r\n', + ); + return; + } + const launcher = + '#!/usr/bin/env sh\nset -e\nROOT="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"\nexec "$ROOT/node/bin/node" "$ROOT/lib/cli-entry.js" "$@"\n'; + const launcherPath = path.join(binDir, 'qwen'); + fs.writeFileSync(launcherPath, launcher); + fs.chmodSync(launcherPath, 0o755); +} + +function copyRequiredFile(source, destination) { + if (!fs.statSync(source, { throwIfNoEntry: false })?.isFile()) { + throw new Error(`Required desktop runtime file is missing: ${source}`); + } + fs.copyFileSync(source, destination); +} + +function gitCommit(directory) { + return execFileSync('git', ['rev-parse', 'HEAD'], { + cwd: directory, + encoding: 'utf8', + }).trim(); +} + +function writeChecksums() { + const checksums = {}; + for (const file of runtimeFiles(packageRoot)) { + const relative = path.relative(packageRoot, file).split(path.sep).join('/'); + if (relative === 'checksums.json') continue; + checksums[relative] = crypto + .createHash('sha256') + .update(fs.readFileSync(file)) + .digest('hex'); + } + fs.writeFileSync( + path.join(packageRoot, 'checksums.json'), + `${JSON.stringify(checksums, null, 2)}\n`, + ); +} + +function runtimeFiles(directory) { + return fs + .readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + const absolute = path.join(directory, entry.name); + return entry.isDirectory() ? runtimeFiles(absolute) : [absolute]; + }) + .sort(); +} + +function copyDirectory(source, destination) { + fs.cpSync(source, destination, { + recursive: true, + dereference: true, + filter: (entry) => path.basename(entry) !== '.DS_Store', + }); +} diff --git a/packages/desktop-shell/scripts/smoke-packaged.js b/packages/desktop-shell/scripts/smoke-packaged.js new file mode 100755 index 00000000000..f7ec4e0b09d --- /dev/null +++ b/packages/desktop-shell/scripts/smoke-packaged.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node + +import { execFileSync, spawn } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const executable = process.argv[2]; +if (!executable) + throw new Error('Usage: node scripts/smoke-packaged.js '); +if (!fs.statSync(executable, { throwIfNoEntry: false })?.isFile()) { + throw new Error(`Packaged executable is missing: ${executable}`); +} + +const workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'qwen-desktop-smoke-')); +const isolatedHome = path.join(workspace, 'home'); +const isolatedState = path.join(workspace, 'state'); +fs.mkdirSync(isolatedHome); +fs.mkdirSync(isolatedState); +const appId = 'com.qwen.code.desktop'; +const logRoot = + process.platform === 'darwin' + ? path.join(isolatedHome, 'Library', 'Logs', appId) + : path.join(isolatedState, appId, 'logs'); +const logPath = path.join(logRoot, 'desktop-runtime.log'); +fs.mkdirSync(logRoot, { recursive: true }); +const child = spawn(executable, [], { + detached: process.platform !== 'win32', + env: { + ...process.env, + QWEN_DESKTOP_WORKSPACE: workspace, + QWEN_CODE_SUPPRESS_YOLO_WARNING: '1', + HOME: isolatedHome, + LOCALAPPDATA: isolatedState, + XDG_STATE_HOME: isolatedState, + XDG_DATA_HOME: isolatedState, + ...(process.platform === 'linux' + ? { NO_AT_BRIDGE: '1', GTK_A11Y: 'none' } + : {}), + ...(process.platform === 'darwin' + ? {} + : { + QWEN_DESKTOP_RUNTIME_DIR: path.join( + packageDir, + 'runtime', + 'qwen-code', + ), + }), + }, + stdio: ['ignore', 'pipe', 'pipe'], +}); +let processOutput = ''; +let completed = false; +let exitFailure; +captureProcessOutput(child.stdout, 'stdout'); +captureProcessOutput(child.stderr, 'stderr'); +child.on('exit', (code, signal) => { + processOutput += `[exit] code=${code ?? 'null'} signal=${signal ?? 'null'}\n`; + exitFailure = new Error( + `Packaged desktop runtime exited before readiness (code ${code ?? 'null'}, signal ${signal ?? 'null'})`, + ); +}); +child.unref(); + +try { + await waitForReady(); + completed = true; + console.log(`Packaged desktop runtime ready: ${executable}`); +} finally { + terminate(child.pid); + if (completed) fs.rmSync(workspace, { recursive: true, force: true }); +} + +function captureProcessOutput(stream, name) { + stream?.on('data', (chunk) => { + if (processOutput.length >= 16 * 1024) return; + processOutput += `[${name}] ${chunk.toString('utf8')}`; + processOutput = processOutput.slice(0, 16 * 1024); + }); +} + +async function waitForReady() { + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + if (exitFailure) throw exitFailure; + const contents = fs.readFileSync(logPath, { + encoding: 'utf8', + flag: 'a+', + }); + const match = contents.match( + /qwen serve listening on (http:\/\/127\.0\.0\.1:\d+)/, + ); + if (match) { + await verifyPackagedShell(match[1], contents); + return; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + const contents = fs.readFileSync(logPath, { + encoding: 'utf8', + flag: 'a+', + }); + throw new Error( + `Timed out waiting for packaged desktop runtime.\n${contents}${processOutput}\nSmoke workspace: ${workspace}`, + ); +} + +// The packaged smoke verifies the unauthenticated navigation boundary: the +// shell HTML is served without a token (the token travels in the URL fragment, +// which never reaches the server), while API routes stay bearer-gated. The +// authenticated path is covered by smoke:runtime on the same bundle. +async function verifyPackagedShell(baseUrl, contents) { + // The daemon starts in deferred-runtime mode; the delegating app 401s + // unauthenticated non-bootstrap requests until the runtime is mounted. + // Retry until the fallback timer starts the runtime and the build finishes. + const deadline = Date.now() + 30_000; + let shell; + do { + if (exitFailure) throw exitFailure; + shell = await fetch(new URL('/', baseUrl), { + redirect: 'manual', + headers: { + Accept: 'text/html', + 'Sec-Fetch-Dest': 'document', + 'Sec-Fetch-Mode': 'navigate', + }, + }); + if (shell.status === 200) break; + await new Promise((resolve) => setTimeout(resolve, 500)); + } while (Date.now() < deadline); + if (shell.status !== 200) { + throw smokeError( + `Packaged desktop Web Shell navigation failed: ${shell.status}`, + contents, + ); + } + if (shell.headers.getSetCookie().length > 0) { + throw smokeError( + 'Packaged desktop Web Shell must not mint auth cookies', + contents, + ); + } + if (!(await shell.text()).includes('')) { + throw smokeError( + 'Packaged desktop Web Shell navigation did not return the HTML shell', + contents, + ); + } + const unauthenticated = await fetch(new URL('/capabilities', baseUrl)); + if (unauthenticated.status !== 401) { + throw smokeError( + `Packaged desktop API is not token-gated: ${unauthenticated.status}`, + contents, + ); + } +} + +function smokeError(message, contents) { + return new Error( + `${message}\n${contents}${processOutput}\nSmoke workspace: ${workspace}`, + ); +} + +function terminate(pid) { + if (!pid) return; + try { + if (process.platform === 'win32') { + execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { + stdio: 'ignore', + }); + } else { + process.kill(-pid, 'SIGTERM'); + } + } catch { + // The process may already have exited after the smoke succeeded or failed. + } +} diff --git a/packages/desktop-shell/scripts/smoke-runtime.js b/packages/desktop-shell/scripts/smoke-runtime.js new file mode 100755 index 00000000000..098227d6328 --- /dev/null +++ b/packages/desktop-shell/scripts/smoke-runtime.js @@ -0,0 +1,168 @@ +#!/usr/bin/env node + +import { spawn } from 'node:child_process'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const runtimeRoot = path.join(packageDir, 'runtime', 'qwen-code'); +const nodePath = + process.platform === 'win32' + ? path.join(runtimeRoot, 'node', 'node.exe') + : path.join(runtimeRoot, 'node', 'bin', 'node'); +const entryPath = path.join(runtimeRoot, 'lib', 'cli-entry.js'); +const token = crypto.randomBytes(32).toString('hex'); + +verifyRuntimeIntegrity(); + +const child = spawn( + nodePath, + [ + entryPath, + 'serve', + '--port', + '0', + '--hostname', + '127.0.0.1', + '--require-auth', + '--workspace', + packageDir, + '--no-open', + ], + { + cwd: packageDir, + env: { ...process.env, QWEN_SERVER_TOKEN: token }, + stdio: ['ignore', 'pipe', 'pipe'], + }, +); + +let output = ''; +let done = false; +let verifying = false; +const timeout = setTimeout( + () => finish(new Error('Timed out waiting for bundled daemon startup')), + 45_000, +); +child.stdout.setEncoding('utf8'); +child.stderr.setEncoding('utf8'); +child.stdout.on('data', (chunk) => { + output += chunk; + const match = output.match(/qwen serve listening on (http:\/\/[^\s]+)/); + if (match && !verifying) { + verifying = true; + void verify(match[1]).catch(finish); + } +}); +child.stderr.on('data', (chunk) => { + output += chunk; +}); +child.on('exit', (code) => { + if (!done) + finish( + new Error( + `Bundled daemon exited before readiness (code ${code})\n${output}`, + ), + ); +}); + +async function verify(baseUrl) { + const response = await fetch(`${baseUrl}/health`, { + headers: { Authorization: `Bearer ${token}` }, + }); + const text = await response.text(); + if (!response.ok || !text.includes('"status":"ok"')) { + finish(new Error(`Health check failed: ${response.status} ${text}`)); + return; + } + // The shallow health probe triggers the deferred runtime start. Wait for + // deep health (served by the runtime app) before asserting the + // unauthenticated shell, which the delegating app 401s until the runtime + // is mounted. + await waitForDeepHealth(baseUrl); + const shell = await fetch(baseUrl, { + headers: { Accept: 'text/html' }, + }); + const html = await shell.text(); + if (!shell.ok || !html.includes('
')) { + finish(new Error(`Web Shell check failed: ${shell.status}`)); + return; + } + console.log(`Bundled daemon and Web Shell ready at ${baseUrl}`); + finish(); +} + +async function waitForDeepHealth(baseUrl) { + const deadline = Date.now() + 30_000; + while (Date.now() < deadline) { + const response = await fetch(`${baseUrl}/health?deep=true`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (response.ok) return; + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error('Timed out waiting for runtime deep health'); +} + +function finish(error) { + if (done) return; + done = true; + clearTimeout(timeout); + child.kill('SIGTERM'); + if (error) { + console.error(error.message); + process.exitCode = 1; + } +} + +function verifyRuntimeIntegrity() { + const required = [ + 'manifest.json', + 'checksums.json', + 'LICENSE', + 'NOTICE', + 'node/LICENSE', + 'lib/cli-entry.js', + 'lib/web-shell/index.html', + ]; + for (const relative of required) { + const file = path.join(runtimeRoot, relative); + if (!fs.statSync(file, { throwIfNoEntry: false })?.isFile()) { + throw new Error(`Bundled runtime file is missing: ${relative}`); + } + } + const manifest = JSON.parse( + fs.readFileSync(path.join(runtimeRoot, 'manifest.json'), 'utf8'), + ); + for (const field of [ + 'desktopVersion', + 'qwenCodeVersion', + 'qwenCodeCommit', + 'target', + 'node', + 'builtAt', + ]) { + if (!manifest[field]) + throw new Error(`Runtime manifest is missing ${field}`); + } + const checksums = JSON.parse( + fs.readFileSync(path.join(runtimeRoot, 'checksums.json'), 'utf8'), + ); + for (const [relative, expected] of Object.entries(checksums)) { + const file = path.join(runtimeRoot, relative); + if (!fs.statSync(file, { throwIfNoEntry: false })?.isFile()) { + throw new Error(`Checksummed runtime file is missing: ${relative}`); + } + const actual = crypto + .createHash('sha256') + .update(fs.readFileSync(file)) + .digest('hex'); + if (actual !== expected) { + throw new Error(`Bundled runtime checksum mismatch: ${relative}`); + } + } +} diff --git a/packages/desktop-shell/scripts/test-release.js b/packages/desktop-shell/scripts/test-release.js new file mode 100755 index 00000000000..9c710e4d894 --- /dev/null +++ b/packages/desktop-shell/scripts/test-release.js @@ -0,0 +1,188 @@ +#!/usr/bin/env node + +import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const repoRoot = path.resolve(packageDir, '../..'); +const manifestScript = path.join( + repoRoot, + '.github', + 'scripts', + 'create-desktop-update-manifest.mjs', +); +const versionScript = path.join(packageDir, 'scripts', 'version.js'); + +const root = fs.mkdtempSync( + path.join(os.tmpdir(), 'qwen-desktop-release-test-'), +); +try { + testBootstrapBridgeConfiguration(); + testUpdateManifest(path.join(root, 'manifest')); + testVersionSynchronization(path.join(root, 'version')); + console.log('Desktop release helper checks passed.'); +} finally { + fs.rmSync(root, { recursive: true, force: true }); +} + +function testBootstrapBridgeConfiguration() { + const config = JSON.parse( + fs.readFileSync( + path.join(packageDir, 'src-tauri', 'tauri.conf.json'), + 'utf8', + ), + ); + assert.equal( + config.app?.withGlobalTauri, + true, + 'The Bootstrap UI requires window.__TAURI__ for desktop commands.', + ); + assert.deepEqual( + config.app?.security?.capabilities, + ['bootstrap'], + 'The Bootstrap UI capability must be enabled for the main window.', + ); + const capability = JSON.parse( + fs.readFileSync( + path.join(packageDir, 'src-tauri', 'capabilities', 'bootstrap.json'), + 'utf8', + ), + ); + assert.deepEqual(capability.windows, ['main']); + assert.equal( + capability.remote, + undefined, + 'The bootstrap capability must not grant remote IPC access.', + ); + assert.deepEqual(capability.permissions, [ + 'core:event:allow-listen', + 'core:event:allow-unlisten', + ]); +} + +function testUpdateManifest(directory) { + const assets = path.join(directory, 'assets'); + fs.mkdirSync(assets, { recursive: true }); + const artifacts = [ + 'Qwen-Code-aarch64-apple-darwin.app.tar.gz', + 'Qwen-Code-x86_64-apple-darwin.app.tar.gz', + 'Qwen-Code_0.1.0_x64-setup.exe', + 'Qwen-Code_0.1.0_amd64.AppImage', + ]; + for (const artifact of artifacts) { + assert.ok( + !artifact.includes(' '), + `Artifact name must not contain spaces: ${artifact}`, + ); + } + for (const artifact of artifacts) { + fs.writeFileSync(path.join(assets, artifact), artifact); + fs.writeFileSync( + path.join(assets, `${artifact}.sig`), + `signature:${artifact}\n`, + ); + } + const output = path.join(directory, 'desktop-latest.json'); + execFileSync(process.execPath, [ + manifestScript, + '--assets', + assets, + '--repository', + 'QwenLM/qwen-code', + '--tag', + 'desktop-v0.1.0', + '--version', + '0.1.0', + '--output', + output, + ]); + const manifest = JSON.parse(fs.readFileSync(output, 'utf8')); + assert.equal(manifest.version, '0.1.0'); + assert.deepEqual(Object.keys(manifest.platforms).sort(), [ + 'darwin-aarch64', + 'darwin-x86_64', + 'linux-x86_64', + 'windows-x86_64', + ]); + for (const [platform, artifact] of [ + ['darwin-aarch64', artifacts[0]], + ['darwin-x86_64', artifacts[1]], + ['windows-x86_64', artifacts[2]], + ['linux-x86_64', artifacts[3]], + ]) { + assert.equal( + manifest.platforms[platform].signature, + `signature:${artifact}`, + ); + assert.equal( + manifest.platforms[platform].url, + `https://github.com/QwenLM/qwen-code/releases/download/desktop-v0.1.0/${encodeURIComponent(artifact)}`, + ); + } + + fs.rmSync(path.join(assets, `${artifacts[3]}.sig`)); + const failure = spawnSync( + process.execPath, + [ + manifestScript, + '--assets', + assets, + '--repository', + 'QwenLM/qwen-code', + '--tag', + 'desktop-v0.1.0', + '--version', + '0.1.0', + '--output', + output, + ], + { encoding: 'utf8' }, + ); + assert.notEqual(failure.status, 0); + assert.match(failure.stderr, /Missing updater signature/); +} + +function testVersionSynchronization(directory) { + fs.mkdirSync(path.join(directory, 'src-tauri'), { recursive: true }); + fs.copyFileSync( + path.join(packageDir, 'package.json'), + path.join(directory, 'package.json'), + ); + fs.copyFileSync( + path.join(packageDir, 'src-tauri', 'Cargo.toml'), + path.join(directory, 'src-tauri', 'Cargo.toml'), + ); + fs.copyFileSync( + path.join(packageDir, 'src-tauri', 'tauri.conf.json'), + path.join(directory, 'src-tauri', 'tauri.conf.json'), + ); + execFileSync(process.execPath, [versionScript, '1.2.3'], { + cwd: directory, + env: { ...process.env, QWEN_DESKTOP_PACKAGE_DIR: directory }, + }); + assert.equal( + JSON.parse(fs.readFileSync(path.join(directory, 'package.json'), 'utf8')) + .version, + '1.2.3', + ); + assert.equal( + JSON.parse( + fs.readFileSync( + path.join(directory, 'src-tauri', 'tauri.conf.json'), + 'utf8', + ), + ).version, + '1.2.3', + ); + assert.match( + fs.readFileSync(path.join(directory, 'src-tauri', 'Cargo.toml'), 'utf8'), + /^version = "1\.2\.3"$/m, + ); +} diff --git a/packages/desktop-shell/scripts/version.js b/packages/desktop-shell/scripts/version.js new file mode 100755 index 00000000000..db58e686a15 --- /dev/null +++ b/packages/desktop-shell/scripts/version.js @@ -0,0 +1,46 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const packageDir = process.env.QWEN_DESKTOP_PACKAGE_DIR + ? path.resolve(process.env.QWEN_DESKTOP_PACKAGE_DIR) + : path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const version = process.argv[2]?.replace(/^v/, ''); +if ( + !version || + !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) +) { + throw new Error('Usage: node scripts/version.js '); +} + +const cargoPath = path.join(packageDir, 'src-tauri', 'Cargo.toml'); +const cargo = fs.readFileSync(cargoPath, 'utf8'); +const cargoVersionPattern = /(^\[package\][\s\S]*?^version = ")([^"]+)("$)/m; +const cargoMatch = cargoVersionPattern.exec(cargo); +if (!cargoMatch) { + throw new Error('Failed to find the Cargo package version.'); +} +if (cargoMatch[2] === version) { + console.log(`Desktop version already set to ${version}`); + process.exit(0); +} + +updateJson(path.join(packageDir, 'package.json'), (manifest) => { + manifest.version = version; +}); +updateJson(path.join(packageDir, 'src-tauri', 'tauri.conf.json'), (config) => { + config.version = version; +}); +fs.writeFileSync( + cargoPath, + cargo.replace(cargoVersionPattern, `$1${version}$3`), +); +console.log(`Desktop version set to ${version}`); + +function updateJson(file, update) { + const value = JSON.parse(fs.readFileSync(file, 'utf8')); + update(value); + fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`); +} diff --git a/packages/desktop-shell/src-tauri/.gitignore b/packages/desktop-shell/src-tauri/.gitignore new file mode 100644 index 00000000000..79c707cbafd --- /dev/null +++ b/packages/desktop-shell/src-tauri/.gitignore @@ -0,0 +1,2 @@ +/target/ +/gen/ diff --git a/packages/desktop-shell/src-tauri/Cargo.lock b/packages/desktop-shell/src-tauri/Cargo.lock new file mode 100644 index 00000000000..8ec31e4efd2 --- /dev/null +++ b/packages/desktop-shell/src-tauri/Cargo.lock @@ -0,0 +1,5539 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb1307f12aa967b5a58416e87b3653360e0fd614a016b6e970db08fecbb1b80d" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "command-group" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68fa787550392a9d58f44c21a3022cfb3ea3e2458b7f85d3b399d0ceeccf409" +dependencies = [ + "nix", + "winapi", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.119", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfdaacccebec3b28e4866b8973543c7647797db5ada1bdab552e48fe665fbbd" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.4+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.19", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" +dependencies = [ + "dunce", + "is-wsl", + "libc", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da1d65da6dd5d1e44199ac0f58712d241c0f439f80adea8924d832384087f85" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "qwen-code-desktop" +version = "0.0.1" +dependencies = [ + "command-group", + "open", + "rand", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-opener", + "tauri-plugin-single-instance", + "tauri-plugin-updater", + "ureq", + "url", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.19", +] + +[[package]] +name = "ref-cast" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.119", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.2", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.1", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni 0.21.1", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7eeb6d99155545da6150a1795945f16ac9c178deb2a5f2e74d776107bd5849" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tar" +version = "0.4.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni 0.21.1", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.19", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.119", + "tauri-utils", + "thiserror 2.0.19", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2d3c1dbe38037e7f590cdf2492594d5ceebe031e7bc7e827509b22a999d2940" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.19", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.19", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3214becf9ef5783c0ae99a3bb25adf5353a7a16ebf53e74b909e29205735c6c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.19", + "tokio", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.19", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni 0.21.1", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f" +dependencies = [ + "gtk", + "http", + "jni 0.21.1", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.19", + "toml 1.1.4+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.4+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.19", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.19", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[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 = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni 0.21.1", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.19", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 1.0.4", +] diff --git a/packages/desktop-shell/src-tauri/Cargo.toml b/packages/desktop-shell/src-tauri/Cargo.toml new file mode 100644 index 00000000000..3495c0256de --- /dev/null +++ b/packages/desktop-shell/src-tauri/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "qwen-code-desktop" +version = "0.0.1" +description = "Thin desktop shell for Qwen Code Web Shell" +authors = ["Qwen Team"] +license = "Apache-2.0" +edition = "2021" +rust-version = "1.77.2" + +[build-dependencies] +tauri-build = { version = "2.4.1", features = [] } + +[dependencies] +command-group = "5.0.1" +open = "5.4.0" +rand = "0.9.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0.151" +tauri = { version = "2.8.5", features = [] } +tauri-plugin-dialog = "2.7.2" +tauri-plugin-opener = "2.5.4" +tauri-plugin-single-instance = "2.4.0" +tauri-plugin-updater = "2.10.1" +ureq = { version = "3.1.2", default-features = false } +url = "2.5.4" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/packages/desktop-shell/src-tauri/Entitlements.plist b/packages/desktop-shell/src-tauri/Entitlements.plist new file mode 100644 index 00000000000..b49b6618def --- /dev/null +++ b/packages/desktop-shell/src-tauri/Entitlements.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/packages/desktop-shell/src-tauri/build.rs b/packages/desktop-shell/src-tauri/build.rs new file mode 100644 index 00000000000..adf32c9b4a4 --- /dev/null +++ b/packages/desktop-shell/src-tauri/build.rs @@ -0,0 +1,6 @@ +fn main() { + let windows = tauri_build::WindowsAttributes::new() + .app_manifest(include_str!("windows-app-manifest.xml")); + let attributes = tauri_build::Attributes::new().windows_attributes(windows); + tauri_build::try_build(attributes).expect("failed to run Tauri build script"); +} diff --git a/packages/desktop-shell/src-tauri/capabilities/.gitkeep b/packages/desktop-shell/src-tauri/capabilities/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/desktop-shell/src-tauri/capabilities/bootstrap.json b/packages/desktop-shell/src-tauri/capabilities/bootstrap.json new file mode 100644 index 00000000000..cae3cb0589a --- /dev/null +++ b/packages/desktop-shell/src-tauri/capabilities/bootstrap.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "bootstrap", + "description": "Allows the local bootstrap page to subscribe to desktop lifecycle events.", + "windows": ["main"], + "permissions": ["core:event:allow-listen", "core:event:allow-unlisten"] +} diff --git a/packages/desktop-shell/src-tauri/icons/128x128.png b/packages/desktop-shell/src-tauri/icons/128x128.png new file mode 100644 index 00000000000..2f051c23866 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/128x128.png differ diff --git a/packages/desktop-shell/src-tauri/icons/128x128@2x.png b/packages/desktop-shell/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000000..a22f46f6a43 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/128x128@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/32x32.png b/packages/desktop-shell/src-tauri/icons/32x32.png new file mode 100644 index 00000000000..e08d3219722 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/32x32.png differ diff --git a/packages/desktop-shell/src-tauri/icons/64x64.png b/packages/desktop-shell/src-tauri/icons/64x64.png new file mode 100644 index 00000000000..a83faac5f6f Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/64x64.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square107x107Logo.png b/packages/desktop-shell/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000000..f05c6769519 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square107x107Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square142x142Logo.png b/packages/desktop-shell/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000000..4caad1fadc1 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square142x142Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square150x150Logo.png b/packages/desktop-shell/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000000..768942ab31d Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square150x150Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square284x284Logo.png b/packages/desktop-shell/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000000..c04f620ffdd Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square284x284Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square30x30Logo.png b/packages/desktop-shell/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000000..c4f56b409e1 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square30x30Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square310x310Logo.png b/packages/desktop-shell/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000000..ab52a3cd3f4 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square310x310Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square44x44Logo.png b/packages/desktop-shell/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000000..34784be3a47 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square44x44Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square71x71Logo.png b/packages/desktop-shell/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000000..bc4a163791a Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square71x71Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/Square89x89Logo.png b/packages/desktop-shell/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000000..635f063c162 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/Square89x89Logo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/StoreLogo.png b/packages/desktop-shell/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000000..ae8bc9cd73b Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/StoreLogo.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/packages/desktop-shell/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000000..2ffbf24b689 --- /dev/null +++ b/packages/desktop-shell/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000000..6dd8bd04e3a Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..b4198e4de48 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000000..b9567a06611 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000000..ab9524effc0 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..503e192ef27 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000000..80309980ee5 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000000..c0bd468d701 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..c5ae79235ea Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..e34e40dee5c Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000000..7770e1e3e87 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..d2d5d55aada Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..92084c8f443 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000000..7982f6d7e73 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000000..6e4078dc82a Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000000..70604cec598 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/packages/desktop-shell/src-tauri/icons/android/values/ic_launcher_background.xml b/packages/desktop-shell/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 00000000000..ea9c223a6cb --- /dev/null +++ b/packages/desktop-shell/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/packages/desktop-shell/src-tauri/icons/icon.icns b/packages/desktop-shell/src-tauri/icons/icon.icns new file mode 100644 index 00000000000..f495984757b Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/icon.icns differ diff --git a/packages/desktop-shell/src-tauri/icons/icon.ico b/packages/desktop-shell/src-tauri/icons/icon.ico new file mode 100644 index 00000000000..6ceb31c618e Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/icon.ico differ diff --git a/packages/desktop-shell/src-tauri/icons/icon.png b/packages/desktop-shell/src-tauri/icons/icon.png new file mode 100644 index 00000000000..32a664bdb27 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/icon.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@1x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 00000000000..54465a9ad87 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 00000000000..c5429685684 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 00000000000..c5429685684 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@3x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 00000000000..97f2bcbe2d2 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@1x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 00000000000..2335af64583 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 00000000000..0db41db49ea Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 00000000000..0db41db49ea Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@3x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 00000000000..8a836371e49 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@1x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 00000000000..c5429685684 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 00000000000..40fd8ad0425 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 00000000000..40fd8ad0425 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@3x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 00000000000..97eb94305c2 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-512@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 00000000000..7157eded2bf Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 00000000000..97eb94305c2 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@3x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 00000000000..70e1081232b Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@1x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 00000000000..95c74d5908a Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 00000000000..c4f145cc03d Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/packages/desktop-shell/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 00000000000..fa7b70ab121 Binary files /dev/null and b/packages/desktop-shell/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/packages/desktop-shell/src-tauri/src/desktop_state.rs b/packages/desktop-shell/src-tauri/src/desktop_state.rs new file mode 100644 index 00000000000..ca7115cfbe7 --- /dev/null +++ b/packages/desktop-shell/src-tauri/src/desktop_state.rs @@ -0,0 +1,286 @@ +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Mutex; +use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, WebviewWindow}; + +const DEFAULT_WIDTH: u32 = 1280; +const DEFAULT_HEIGHT: u32 = 820; +const MIN_WIDTH: u32 = 900; +const MIN_HEIGHT: u32 = 600; +static NEXT_WRITE_ID: AtomicU64 = AtomicU64::new(1); + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(default)] +pub struct DesktopSettings { + pub workspace: Option, + pub window: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct WindowState { + pub width: u32, + pub height: u32, + pub x: i32, + pub y: i32, + pub maximized: bool, +} + +pub struct SettingsStore { + path: PathBuf, + settings: Mutex, +} + +impl SettingsStore { + pub fn load(app: &AppHandle) -> Result { + let path = settings_path(app)?; + let settings = match fs::read_to_string(&path) { + Ok(contents) => parse_settings(&contents), + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + DesktopSettings::default() + } + Err(error) => return Err(format!("Failed to read desktop settings: {error}")), + }; + Ok(Self { + path, + settings: Mutex::new(settings), + }) + } + + pub fn workspace(&self) -> Option { + self.with_settings(|settings| settings.workspace.clone()) + } + + pub fn set_workspace(&self, workspace: PathBuf) -> Result<(), String> { + self.update(|settings| settings.workspace = Some(workspace)) + } + + pub fn window(&self) -> Option { + self.with_settings(|settings| settings.window.clone()) + } + + pub fn save_window(&self, window: &WebviewWindow) -> Result<(), String> { + let position = window + .outer_position() + .map_err(|error| format!("Failed to read window position: {error}"))?; + let size = window + .inner_size() + .map_err(|error| format!("Failed to read window size: {error}"))?; + let maximized = window + .is_maximized() + .map_err(|error| format!("Failed to read window maximized state: {error}"))?; + self.update(|settings| { + settings.window = Some(saved_window_state( + settings.window.as_ref(), + position, + size, + maximized, + )); + }) + } + + fn update(&self, update: impl FnOnce(&mut DesktopSettings)) -> Result<(), String> { + let mut settings = match self.settings.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + update(&mut settings); + let serialized = serde_json::to_string_pretty(&*settings) + .map_err(|error| format!("Failed to serialize desktop settings: {error}"))?; + write_atomic(&self.path, format!("{serialized}\n").as_bytes()) + } + + fn with_settings(&self, read: impl FnOnce(&DesktopSettings) -> T) -> T { + let settings = match self.settings.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + read(&settings) + } +} + +pub fn restore_window(window: &WebviewWindow, state: Option<&WindowState>) { + let Some(state) = state else { + let _ = window.center(); + return; + }; + let size = PhysicalSize::new(state.width.max(MIN_WIDTH), state.height.max(MIN_HEIGHT)); + let _ = window.set_size(size); + if window + .monitor_from_point(f64::from(state.x), f64::from(state.y)) + .ok() + .flatten() + .is_some() + { + let _ = window.set_position(PhysicalPosition::new(state.x, state.y)); + } else { + let _ = window.center(); + } + if state.maximized { + let _ = window.maximize(); + } +} + +pub fn default_window_size() -> (f64, f64) { + (f64::from(DEFAULT_WIDTH), f64::from(DEFAULT_HEIGHT)) +} + +fn settings_path(app: &AppHandle) -> Result { + app.path() + .app_config_dir() + .map(|path| path.join("desktop-state.json")) + .map_err(|error| format!("Failed to resolve desktop settings directory: {error}")) +} + +fn parse_settings(contents: &str) -> DesktopSettings { + serde_json::from_str(contents).unwrap_or_default() +} + +fn saved_window_state( + previous: Option<&WindowState>, + position: PhysicalPosition, + size: PhysicalSize, + maximized: bool, +) -> WindowState { + if maximized { + if let Some(previous) = previous { + return WindowState { + maximized: true, + ..previous.clone() + }; + } + return WindowState { + width: DEFAULT_WIDTH, + height: DEFAULT_HEIGHT, + x: position.x, + y: position.y, + maximized: true, + }; + } + WindowState { + width: size.width.max(MIN_WIDTH), + height: size.height.max(MIN_HEIGHT), + x: position.x, + y: position.y, + maximized, + } +} + +fn write_atomic(path: &Path, contents: &[u8]) -> Result<(), String> { + let parent = path + .parent() + .ok_or_else(|| "Desktop settings path has no parent directory.".to_string())?; + fs::create_dir_all(parent) + .map_err(|error| format!("Failed to create desktop settings directory: {error}"))?; + let temporary = path.with_extension(format!( + "json.{}.tmp", + NEXT_WRITE_ID.fetch_add(1, Ordering::Relaxed) + )); + fs::write(&temporary, contents) + .map_err(|error| format!("Failed to write desktop settings: {error}"))?; + if let Err(error) = fs::rename(&temporary, path) { + if cfg!(windows) && path.exists() { + let backup = path.with_extension(format!( + "json.{}.bak", + NEXT_WRITE_ID.fetch_add(1, Ordering::Relaxed) + )); + fs::rename(path, &backup).map_err(|backup_error| { + format!("Failed to prepare desktop settings replacement: {backup_error}") + })?; + if let Err(rename_error) = fs::rename(&temporary, path) { + let _ = fs::rename(&backup, path); + return Err(format!( + "Failed to replace desktop settings: {rename_error}" + )); + } + let _ = fs::remove_file(backup); + } else { + return Err(format!("Failed to replace desktop settings: {error}")); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::{parse_settings, saved_window_state, write_atomic, DesktopSettings, WindowState}; + use std::fs; + use tauri::{PhysicalPosition, PhysicalSize}; + + #[test] + fn settings_remain_backward_compatible_when_fields_are_missing() { + let settings: DesktopSettings = serde_json::from_str("{}").expect("settings"); + assert!(settings.workspace.is_none()); + assert!(settings.window.is_none()); + } + + #[test] + fn corrupt_settings_fall_back_to_defaults() { + let settings = parse_settings("{"); + assert!(settings.workspace.is_none()); + assert!(settings.window.is_none()); + } + + #[test] + fn window_state_round_trips() { + let state = WindowState { + width: 1200, + height: 800, + x: 20, + y: 40, + maximized: true, + }; + let json = serde_json::to_string(&state).expect("serialize"); + let restored: WindowState = serde_json::from_str(&json).expect("deserialize"); + assert_eq!(restored.width, 1200); + assert!(restored.maximized); + } + + #[test] + fn maximized_save_preserves_previous_normal_bounds() { + let previous = WindowState { + width: 1000, + height: 700, + x: 10, + y: 20, + maximized: false, + }; + let state = saved_window_state( + Some(&previous), + PhysicalPosition::new(0, 0), + PhysicalSize::new(1920, 1080), + true, + ); + assert_eq!(state.width, 1000); + assert_eq!(state.height, 700); + assert_eq!(state.x, 10); + assert_eq!(state.y, 20); + assert!(state.maximized); + } + + #[test] + fn maximized_first_save_uses_default_normal_size() { + let state = saved_window_state( + None, + PhysicalPosition::new(40, 50), + PhysicalSize::new(2560, 1440), + true, + ); + assert_eq!(state.width, 1280); + assert_eq!(state.height, 820); + assert_eq!(state.x, 40); + assert_eq!(state.y, 50); + assert!(state.maximized); + } + + #[test] + fn atomic_write_replaces_existing_contents() { + let root = std::env::temp_dir().join(format!("qwen-desktop-state-{}", std::process::id())); + let path = root.join("desktop-state.json"); + write_atomic(&path, b"first").expect("first write"); + write_atomic(&path, b"second").expect("second write"); + assert_eq!(fs::read_to_string(&path).expect("read"), "second"); + fs::remove_dir_all(root).expect("cleanup"); + } +} diff --git a/packages/desktop-shell/src-tauri/src/main.rs b/packages/desktop-shell/src-tauri/src/main.rs new file mode 100755 index 00000000000..91bfd575920 --- /dev/null +++ b/packages/desktop-shell/src-tauri/src/main.rs @@ -0,0 +1,678 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod desktop_state; +mod runtime; + +use desktop_state::{default_window_size, restore_window, SettingsStore}; +use runtime::DesktopRuntime; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; +use tauri::webview::{DownloadEvent, NewWindowResponse, WebviewWindowBuilder}; +use tauri::{ + AppHandle, Emitter, Listener, Manager, RunEvent, State, WebviewUrl, WebviewWindow, + WindowEvent, +}; +use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind}; +use tauri_plugin_updater::UpdaterExt; +use url::Url; + +#[cfg(target_os = "windows")] +const BOOTSTRAP_URL: &str = "http://tauri.localhost"; +#[cfg(not(target_os = "windows"))] +const BOOTSTRAP_URL: &str = "tauri://localhost"; + +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase")] +struct BootstrapState { + desktop_version: String, + status: &'static str, + workspace: Option, + error: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RuntimeStopped { + runtime_id: u64, + status: String, +} + +struct ApplicationState { + runtime: Mutex>, + settings: SettingsStore, + log_path: PathBuf, + origin: Arc>>, + last_error: Mutex>, + window_dirty: AtomicBool, + start_generation: AtomicU64, + starting: AtomicU64, +} + +fn main() { + let builder = tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + focus_main_window(app); + })) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .invoke_handler(tauri::generate_handler![ + bootstrap_state, + choose_workspace, + open_logs, + restart_runtime, + install_update, + ]) + .setup(setup_app); + + let app = match builder.build(tauri::generate_context!()) { + Ok(app) => app, + Err(error) => { + eprintln!("Failed to initialize Qwen Code desktop: {error}"); + return; + } + }; + + app.run(|app_handle, event| match event { + RunEvent::WindowEvent { label, event, .. } if label == "main" => match event { + WindowEvent::Moved(_) | WindowEvent::Resized(_) => { + app_handle + .state::() + .window_dirty + .store(true, Ordering::Relaxed); + } + WindowEvent::CloseRequested { .. } => save_window_state(app_handle), + _ => {} + }, + RunEvent::Exit | RunEvent::ExitRequested { .. } => { + save_window_state(app_handle); + stop_runtime(app_handle); + } + _ => {} + }); +} + +fn setup_app(app: &mut tauri::App) -> Result<(), Box> { + let handle = app.handle().clone(); + let settings = SettingsStore::load(&handle).map_err(std::io::Error::other)?; + let window_state = settings.window(); + let log_path = desktop_log_path(&handle).map_err(std::io::Error::other)?; + if let Some(parent) = log_path.parent() { + let _ = fs::create_dir_all(parent); + } + let _ = fs::write(&log_path, b""); + let origin = Arc::new(Mutex::new(None)); + let navigation_origin = Arc::clone(&origin); + let runtime_exit_handle = handle.clone(); + handle.listen("runtime-process-stopped", move |event| { + let Ok(stopped) = serde_json::from_str::(event.payload()) else { + return; + }; + let state = runtime_exit_handle.state::(); + if lock(&state.runtime).as_ref().map(DesktopRuntime::id) != Some(stopped.runtime_id) { + return; + } + stop_runtime(&runtime_exit_handle); + *lock(&state.origin) = None; + let message = format!("Qwen Code stopped: {}", stopped.status); + *lock(&state.last_error) = Some(message.clone()); + let _ = navigate_to_bootstrap(&runtime_exit_handle); + let _ = runtime_exit_handle.emit("runtime-failed", message); + }); + let (width, height) = default_window_size(); + + let window = WebviewWindowBuilder::new(&handle, "main", WebviewUrl::App("index.html".into())) + .title("Qwen Code") + .inner_size(width, height) + .min_inner_size(900.0, 600.0) + .on_navigation(move |url| is_allowed_navigation(url, &navigation_origin)) + .on_new_window(|url, _features| { + if is_safe_external_url(&url) { + let _ = open::that_detached(url.as_str()); + } + NewWindowResponse::Deny + }) + .on_download(|webview, event| match event { + DownloadEvent::Requested { url, .. } => webview + .url() + .ok() + .and_then(|current| origin_of(¤t).ok()) + .is_some_and(|current_origin| { + url.scheme() == "blob" + && lock(&webview.app_handle().state::().origin) + .as_ref() + .is_some_and(|runtime_origin| current_origin == *runtime_origin) + }), + DownloadEvent::Finished { .. } => true, + _ => false, + }) + .build()?; + restore_window(&window, window_state.as_ref()); + + handle.manage(ApplicationState { + runtime: Mutex::new(None), + settings, + log_path, + origin, + last_error: Mutex::new(None), + window_dirty: AtomicBool::new(false), + start_generation: AtomicU64::new(0), + starting: AtomicU64::new(0), + }); + + if let Some(workspace) = initial_workspace(&handle) { + start_runtime_async(handle.clone(), workspace); + } else { + let _ = handle.emit("workspace-required", ()); + } + check_updates_silently(handle.clone()); + spawn_window_state_flusher(handle); + Ok(()) +} + +#[tauri::command] +fn bootstrap_state( + webview: WebviewWindow, + state: State<'_, ApplicationState>, +) -> Result { + require_bootstrap_origin(&webview)?; + let starting = state.starting.load(Ordering::SeqCst) != 0; + let running = lock(&state.runtime).is_some(); + Ok(BootstrapState { + desktop_version: env!("CARGO_PKG_VERSION").to_string(), + status: if running { + "ready" + } else if starting { + "starting" + } else { + "idle" + }, + workspace: state + .settings + .workspace() + .map(|path| path.to_string_lossy().into_owned()), + error: lock(&state.last_error).clone(), + }) +} + +#[tauri::command] +async fn choose_workspace( + webview: WebviewWindow, + app: AppHandle, +) -> Result, String> { + require_bootstrap_origin(&webview)?; + let folder = tauri::async_runtime::spawn_blocking({ + let app = app.clone(); + move || { + app.dialog() + .file() + .set_title("Choose a Qwen Code workspace") + .blocking_pick_folder() + } + }) + .await + .map_err(|error| format!("Failed to show workspace picker: {error}"))?; + let Some(folder) = folder else { + return Ok(None); + }; + let workspace = folder + .into_path() + .map_err(|error| format!("Failed to read selected workspace: {error}"))?; + start_runtime_async(app, workspace.clone()); + Ok(Some(workspace.to_string_lossy().into_owned())) +} + +#[tauri::command] +fn restart_runtime(webview: WebviewWindow, app: AppHandle) -> Result<(), String> { + require_bootstrap_origin(&webview)?; + let workspace = app + .state::() + .settings + .workspace() + .ok_or_else(|| "Choose a workspace before starting Qwen Code.".to_string())?; + start_runtime_async(app, workspace); + Ok(()) +} + +#[tauri::command] +fn open_logs( + webview: WebviewWindow, + state: State<'_, ApplicationState>, +) -> Result<(), String> { + require_bootstrap_origin(&webview)?; + if let Some(parent) = state.log_path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("Failed to create desktop log directory: {error}"))?; + } + if !state.log_path.exists() { + fs::write(&state.log_path, b"") + .map_err(|error| format!("Failed to create desktop log: {error}"))?; + } + open::that_detached(&state.log_path) + .map_err(|error| format!("Failed to open desktop logs: {error}")) +} + +#[tauri::command] +async fn install_update(webview: WebviewWindow, app: AppHandle) -> Result<(), String> { + require_bootstrap_origin(&webview)?; + let update = app + .updater() + .map_err(|error| format!("Failed to initialize updater: {error}"))? + .check() + .await + .map_err(|error| format!("Failed to check for updates: {error}"))? + .ok_or_else(|| "No desktop update is available.".to_string())?; + let version = update.version.clone(); + let confirmed = tauri::async_runtime::spawn_blocking({ + let app = app.clone(); + move || { + app.dialog() + .message(format!( + "Install Qwen Code Desktop {version} and restart now?" + )) + .title("Qwen Code update") + .kind(MessageDialogKind::Info) + .buttons(MessageDialogButtons::OkCancelCustom( + "Install and restart".to_string(), + "Cancel".to_string(), + )) + .blocking_show() + } + }) + .await + .map_err(|error| format!("Failed to show update confirmation: {error}"))?; + if !confirmed { + return Ok(()); + } + update + .download_and_install(|_, _| {}, || {}) + .await + .map_err(|error| format!("Failed to install update: {error}"))?; + app.request_restart(); + Ok(()) +} + +fn start_runtime_async(app: AppHandle, workspace: PathBuf) { + let generation = { + let state = app.state::(); + let generation = state.start_generation.fetch_add(1, Ordering::SeqCst) + 1; + state.starting.store(generation, Ordering::SeqCst); + generation + }; + stop_runtime(&app); + *lock(&app.state::().last_error) = None; + let _ = app.emit("runtime-starting", workspace.to_string_lossy().into_owned()); + tauri::async_runtime::spawn_blocking(move || { + let state = app.state::(); + let canonical = match fs::canonicalize(&workspace) { + Ok(path) if path.is_dir() => path, + Ok(path) => { + emit_runtime_failure( + &app, + generation, + format!("Workspace is not a directory: {}", path.display()), + ); + return; + } + Err(error) => { + emit_runtime_failure( + &app, + generation, + format!("Failed to open workspace {}: {error}", workspace.display()), + ); + return; + } + }; + if let Err(error) = state.settings.set_workspace(canonical.clone()) { + emit_runtime_failure(&app, generation, error); + return; + } + match DesktopRuntime::start(&app, &canonical, &state.log_path) { + Ok(runtime) => { + if state.start_generation.load(Ordering::SeqCst) != generation { + runtime.stop(); + return; + } + let origin = match origin_of(runtime.base_url()) { + Ok(origin) => origin, + Err(error) => { + runtime.stop(); + emit_runtime_failure(&app, generation, error); + return; + } + }; + *lock(&state.origin) = Some(origin); + let Some(window) = app.get_webview_window("main") else { + runtime.stop(); + emit_runtime_failure( + &app, + generation, + "Desktop window is unavailable.".to_string(), + ); + return; + }; + if let Err(error) = window.navigate(runtime.authenticated_web_url()) { + runtime.stop(); + emit_runtime_failure( + &app, + generation, + format!("Failed to authenticate and load Web Shell: {error}"), + ); + return; + } + *lock(&state.runtime) = Some(runtime); + state + .starting + .compare_exchange(generation, 0, Ordering::SeqCst, Ordering::SeqCst) + .ok(); + let _ = app.emit("runtime-ready", canonical.to_string_lossy().into_owned()); + } + Err(error) => emit_runtime_failure(&app, generation, error), + } + }); +} + +fn emit_runtime_failure(app: &AppHandle, generation: u64, error: String) { + let state = app.state::(); + if state.start_generation.load(Ordering::SeqCst) != generation { + return; + } + state + .starting + .compare_exchange(generation, 0, Ordering::SeqCst, Ordering::SeqCst) + .ok(); + *lock(&state.origin) = None; + *lock(&state.last_error) = Some(error.clone()); + let _ = navigate_to_bootstrap(app); + let _ = app.emit("runtime-failed", error); +} + +fn stop_runtime(app: &AppHandle) { + if let Some(runtime) = lock(&app.state::().runtime).take() { + runtime.stop(); + } +} + +fn initial_workspace(app: &AppHandle) -> Option { + std::env::var_os("QWEN_DESKTOP_WORKSPACE") + .map(PathBuf::from) + .or_else(|| app.state::().settings.workspace()) +} + +fn desktop_log_path(app: &AppHandle) -> Result { + app.path() + .app_log_dir() + .map(|path| path.join("desktop-runtime.log")) + .map_err(|error| format!("Failed to resolve desktop log directory: {error}")) +} + +fn save_window_state(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = app + .state::() + .settings + .save_window(&window); + } +} + +fn spawn_window_state_flusher(app: AppHandle) { + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_millis(300)); + let state = app.state::(); + if state.window_dirty.swap(false, Ordering::Relaxed) { + save_window_state(&app); + } + }); +} + +fn focus_main_window(app: &AppHandle) { + if let Some(window) = app.get_webview_window("main") { + let _ = window.unminimize(); + let _ = window.show(); + let _ = window.set_focus(); + } +} + +fn navigate_to_bootstrap(app: &AppHandle) -> Result<(), String> { + let url = Url::parse(BOOTSTRAP_URL) + .map_err(|error| format!("Failed to construct bootstrap URL: {error}"))?; + app.get_webview_window("main") + .ok_or_else(|| "Desktop window is unavailable.".to_string())? + .navigate(url) + .map_err(|error| format!("Failed to show desktop recovery page: {error}")) +} + +fn require_bootstrap_origin(webview: &WebviewWindow) -> Result<(), String> { + let url = webview + .url() + .map_err(|error| format!("Failed to read calling webview URL: {error}"))?; + if is_bootstrap_url(&url) { + Ok(()) + } else { + Err("This command is only available from the desktop shell.".to_string()) + } +} + +fn is_allowed_navigation(url: &Url, origin: &Mutex>) -> bool { + is_bootstrap_url(url) + || lock(origin) + .as_ref() + .is_some_and(|allowed| is_same_origin(url, allowed)) +} + +fn is_bootstrap_url(url: &Url) -> bool { + if url.scheme() == "tauri" && url.host_str() == Some("localhost") { + return true; + } + cfg!(target_os = "windows") + && matches!(url.scheme(), "http" | "https") + && url.host_str() == Some("tauri.localhost") +} + +fn origin_of(url: &Url) -> Result { + let mut origin = url.clone(); + origin.set_path("/"); + origin.set_query(None); + origin.set_fragment(None); + if origin.scheme() != "http" || origin.host_str() != Some("127.0.0.1") { + return Err(format!("Refusing non-loopback runtime URL: {origin}")); + } + Ok(origin) +} + +fn is_same_origin(url: &Url, origin: &Url) -> bool { + url.scheme() == origin.scheme() + && url.host_str() == origin.host_str() + && url.port_or_known_default() == origin.port_or_known_default() +} + +fn check_updates_silently(app: AppHandle) { + if cfg!(debug_assertions) { + return; + } + tauri::async_runtime::spawn(async move { + let updater = match app.updater() { + Ok(updater) => updater, + Err(_) => return, + }; + let Ok(Some(update)) = updater.check().await else { + return; + }; + let _ = app.emit("update-available", update.version.clone()); + let version = update.version.clone(); + let confirmed = tauri::async_runtime::spawn_blocking({ + let app = app.clone(); + move || { + app.dialog() + .message(format!( + "Qwen Code Desktop {version} is available. Install and restart now?" + )) + .title("Qwen Code update") + .kind(MessageDialogKind::Info) + .buttons(MessageDialogButtons::OkCancelCustom( + "Install and restart".to_string(), + "Later".to_string(), + )) + .blocking_show() + } + }) + .await; + if !matches!(confirmed, Ok(true)) { + return; + } + if update + .download_and_install(|_, _| {}, || {}) + .await + .is_err() + { + return; + } + app.request_restart(); + }); +} + +fn is_safe_external_url(url: &Url) -> bool { + matches!(url.scheme(), "https" | "http" | "mailto") +} + +fn lock(mutex: &Mutex) -> std::sync::MutexGuard<'_, T> { + match mutex.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +#[cfg(test)] +mod tests { + use super::{ + is_allowed_navigation, is_bootstrap_url, is_safe_external_url, is_same_origin, origin_of, + BOOTSTRAP_URL, + }; + use std::sync::Mutex; + use url::Url; + + #[test] + fn allows_only_the_daemon_origin_in_the_main_window() { + let origin = Url::parse("http://127.0.0.1:49152/").expect("origin"); + assert!(is_same_origin( + &Url::parse("http://127.0.0.1:49152/session/123").expect("same origin"), + &origin, + )); + assert!(!is_same_origin( + &Url::parse("http://127.0.0.1:49153/").expect("different port"), + &origin, + )); + assert!(!is_same_origin( + &Url::parse("https://example.com/").expect("external"), + &origin, + )); + } + + #[test] + fn allows_platform_bootstrap_origins() { + assert!(is_bootstrap_url( + &Url::parse("tauri://localhost/").expect("tauri bootstrap") + )); + if cfg!(target_os = "windows") { + assert!(is_bootstrap_url( + &Url::parse("http://tauri.localhost/").expect("windows bootstrap") + )); + } else { + assert!(!is_bootstrap_url( + &Url::parse("http://tauri.localhost/").expect("not a bootstrap origin") + )); + } + } + + #[test] + fn recovery_uses_the_platform_bootstrap_origin() { + let expected = if cfg!(windows) { + "http://tauri.localhost" + } else { + "tauri://localhost" + }; + assert_eq!(BOOTSTRAP_URL, expected); + } + + #[test] + fn rejects_non_loopback_runtime_origins() { + let error = origin_of(&Url::parse("http://0.0.0.0:4170/").expect("url")) + .expect_err("non-loopback origin"); + assert!(error.contains("non-loopback")); + } + + #[test] + fn new_windows_allow_only_browser_safe_schemes() { + assert!(is_safe_external_url( + &Url::parse("https://qwen.ai/").expect("https") + )); + assert!(is_safe_external_url( + &Url::parse("mailto:test@example.com").expect("mailto") + )); + assert!(!is_safe_external_url( + &Url::parse("file:///etc/passwd").expect("file") + )); + assert!(!is_safe_external_url( + &Url::parse("javascript:alert(1)").expect("javascript") + )); + } + + #[test] + fn allows_bootstrap_but_not_a_runtime_url_before_origin_is_set() { + let origin = Mutex::new(None); + assert!(is_allowed_navigation( + &Url::parse(BOOTSTRAP_URL).expect("bootstrap"), + &origin, + )); + assert!(!is_allowed_navigation( + &Url::parse("http://127.0.0.1:49152/").expect("runtime"), + &origin, + )); + } + + #[test] + fn allows_only_the_recorded_origin_once_it_is_set() { + let origin = Mutex::new(Some( + Url::parse("http://127.0.0.1:49152/").expect("origin"), + )); + assert!(is_allowed_navigation( + &Url::parse("http://127.0.0.1:49152/session/123").expect("same origin"), + &origin, + )); + assert!(!is_allowed_navigation( + &Url::parse("http://127.0.0.1:49153/").expect("different port"), + &origin, + )); + assert!(!is_allowed_navigation( + &Url::parse("https://example.com/").expect("external"), + &origin, + )); + } + + #[test] + fn allows_bootstrap_even_after_origin_is_set() { + let origin = Mutex::new(Some( + Url::parse("http://127.0.0.1:49152/").expect("origin"), + )); + assert!(is_allowed_navigation( + &Url::parse(BOOTSTRAP_URL).expect("bootstrap"), + &origin, + )); + } + + #[test] + fn command_origin_gate_accepts_only_bootstrap() { + assert!(is_bootstrap_url( + &Url::parse(BOOTSTRAP_URL).expect("bootstrap") + )); + assert!(!is_bootstrap_url( + &Url::parse("http://127.0.0.1:49152/").expect("runtime") + )); + assert!(!is_bootstrap_url( + &Url::parse("https://example.com/").expect("external") + )); + } +} diff --git a/packages/desktop-shell/src-tauri/src/runtime.rs b/packages/desktop-shell/src-tauri/src/runtime.rs new file mode 100644 index 00000000000..b559df10aa1 --- /dev/null +++ b/packages/desktop-shell/src-tauri/src/runtime.rs @@ -0,0 +1,547 @@ +use command_group::{CommandGroup, GroupChild}; +use rand::RngCore; +use std::ffi::OsString; +use std::fs::{self, File}; +use std::io::{BufRead, BufReader, Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, +}; +use std::thread; +use std::time::{Duration, Instant}; +use tauri::{AppHandle, Emitter, Manager}; +use url::Url; + +const LISTEN_PREFIX: &str = "qwen serve listening on "; +const STARTUP_TIMEOUT: Duration = Duration::from_secs(45); +const HEALTH_RETRY_INTERVAL: Duration = Duration::from_millis(100); +const HEALTH_REQUEST_TIMEOUT: Duration = Duration::from_secs(2); +const FAILURE_OUTPUT_LIMIT: usize = 16 * 1024; +static NEXT_RUNTIME_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1); + +#[derive(Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +struct RuntimeStopped { + runtime_id: u64, + status: String, +} + +pub struct DesktopRuntime { + id: u64, + pub base_url: Url, + token: String, + child: Arc>>, + stopping: Arc, +} + +impl DesktopRuntime { + pub fn id(&self) -> u64 { + self.id + } + + pub fn start(app: &AppHandle, workspace: &Path, log_path: &Path) -> Result { + let id = NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed); + let layout = RuntimeLayout::resolve(app)?; + let workspace = resolve_workspace(workspace)?; + let token = random_token(); + let mut command = Command::new(&layout.node); + command + .arg(&layout.entry) + .args(runtime_arguments(&workspace)) + .current_dir(&workspace) + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .env("QWEN_CODE_DESKTOP", "1") + .env("QWEN_SERVER_TOKEN", &token); + + let mut child = command + .group_spawn() + .map_err(|error| format!("Failed to start bundled Qwen Code runtime: {error}"))?; + let Some(stdout) = child.inner().stdout.take() else { + stop_runtime_child(&mut child); + return Err("Bundled runtime stdout was not captured.".to_string()); + }; + let Some(stderr) = child.inner().stderr.take() else { + stop_runtime_child(&mut child); + return Err("Bundled runtime stderr was not captured.".to_string()); + }; + let failure_output = Arc::new(Mutex::new(String::new())); + let log = match open_log(log_path) { + Ok(log) => Arc::new(Mutex::new(log)), + Err(error) => { + stop_runtime_child(&mut child); + return Err(error); + } + }; + let (listen_sender, listen_receiver) = std::sync::mpsc::channel(); + capture_stdout( + stdout, + Arc::clone(&failure_output), + Arc::clone(&log), + listen_sender, + ); + capture_stderr(stderr, Arc::clone(&failure_output), log); + + let base_url = + match wait_for_listening(&mut child, listen_receiver, &token, &failure_output) { + Ok(url) => url, + Err(error) => { + let _ = child.kill(); + let _ = child.wait(); + return Err(error); + } + }; + let child = Arc::new(Mutex::new(Some(child))); + let stopping = Arc::new(AtomicBool::new(false)); + monitor_runtime(app.clone(), id, Arc::clone(&child), Arc::clone(&stopping)); + + Ok(Self { + id, + base_url, + token, + child, + stopping, + }) + } + + pub fn stop(&self) { + self.stopping.store(true, Ordering::SeqCst); + let child = match self.child.lock() { + Ok(mut guard) => guard.take(), + Err(poisoned) => poisoned.into_inner().take(), + }; + if let Some(mut child) = child { + let _ = child.kill(); + let _ = child.wait(); + } + } + + pub fn base_url(&self) -> &Url { + &self.base_url + } + + pub fn authenticated_web_url(&self) -> Url { + let mut url = self.base_url.clone(); + url.set_fragment(Some(&format!("token={}", self.token))); + url + } +} + +impl Drop for DesktopRuntime { + fn drop(&mut self) { + self.stop(); + } +} + +struct RuntimeLayout { + node: PathBuf, + entry: PathBuf, +} + +impl RuntimeLayout { + fn resolve(app: &AppHandle) -> Result { + let root = if let Some(path) = std::env::var_os("QWEN_DESKTOP_RUNTIME_DIR") { + PathBuf::from(path) + } else if cfg!(debug_assertions) { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("runtime") + .join("qwen-code") + } else { + app.path() + .resource_dir() + .map_err(|error| format!("Failed to resolve desktop resources: {error}"))? + .join("runtime") + .join("qwen-code") + }; + let node = if cfg!(windows) { + root.join("node").join("node.exe") + } else { + root.join("node").join("bin").join("node") + }; + let entry = root.join("lib").join("cli-entry.js"); + require_file(&node, "Node.js runtime")?; + require_file(&entry, "Qwen Code runtime entry")?; + Ok(Self { node, entry }) + } +} + +fn require_file(path: &Path, description: &str) -> Result<(), String> { + if path.is_file() { + return Ok(()); + } + Err(format!("{description} is missing at {}", path.display())) +} + +fn resolve_workspace(configured: &Path) -> Result { + let workspace = fs::canonicalize(configured).map_err(|error| { + format!( + "Failed to resolve desktop workspace {}: {error}", + configured.display() + ) + })?; + if workspace.is_dir() { + Ok(workspace) + } else { + Err(format!( + "Desktop workspace is not a directory: {}", + workspace.display() + )) + } +} + +fn random_token() -> String { + use std::fmt::Write as _; + let mut bytes = [0_u8; 32]; + rand::rng().fill_bytes(&mut bytes); + let mut token = String::with_capacity(bytes.len() * 2); + for byte in bytes { + let _ = write!(token, "{byte:02x}"); + } + token +} + +fn capture_stdout( + stdout: impl Read + Send + 'static, + failure_output: Arc>, + log: Arc>, + listen_sender: std::sync::mpsc::Sender, +) { + thread::spawn(move || { + for line in BufReader::new(stdout).lines().map_while(Result::ok) { + append_failure_output(&failure_output, &line); + append_log(&log, "stdout", &line); + if let Some(url) = parse_listening_url(&line) { + let _ = listen_sender.send(url); + } + } + }); +} + +fn capture_stderr( + stderr: impl Read + Send + 'static, + failure_output: Arc>, + log: Arc>, +) { + thread::spawn(move || { + for line in BufReader::new(stderr).lines().map_while(Result::ok) { + append_failure_output(&failure_output, &line); + append_log(&log, "stderr", &line); + } + }); +} + +fn append_failure_output(output: &Mutex, line: &str) { + let mut output = match output.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + if output.len() >= FAILURE_OUTPUT_LIMIT { + return; + } + let remaining = FAILURE_OUTPUT_LIMIT - output.len(); + let mut end = line.len().min(remaining); + while end > 0 && !line.is_char_boundary(end) { + end -= 1; + } + if end == 0 { + return; + } + output.push_str(&line[..end]); + if output.len() < FAILURE_OUTPUT_LIMIT { + output.push('\n'); + } +} + +fn open_log(path: &Path) -> Result { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent) + .map_err(|error| format!("Failed to create desktop log directory: {error}"))?; + } + File::options() + .create(true) + .append(true) + .open(path) + .map_err(|error| format!("Failed to open desktop runtime log: {error}")) +} + +fn stop_runtime_child(child: &mut GroupChild) { + let _ = child.kill(); + let _ = child.wait(); +} + +fn append_log(log: &Mutex, stream: &str, line: &str) { + let mut log = match log.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let _ = writeln!(log, "[{stream}] {line}"); + let _ = log.flush(); +} + +fn monitor_runtime( + app: AppHandle, + id: u64, + child: Arc>>, + stopping: Arc, +) { + thread::spawn(move || loop { + thread::sleep(Duration::from_millis(250)); + if stopping.load(Ordering::SeqCst) { + return; + } + let exit = { + let mut guard = match child.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + }; + let Some(process) = guard.as_mut() else { + return; + }; + match process.try_wait() { + Ok(Some(status)) => { + if let Some(mut process) = guard.take() { + let _ = process.kill(); + } + Some(status.to_string()) + } + Ok(None) => None, + Err(error) => { + if let Some(mut process) = guard.take() { + let _ = process.kill(); + } + Some(format!("failed to inspect daemon: {error}")) + } + } + }; + if let Some(status) = exit { + if !stopping.load(Ordering::SeqCst) { + let _ = app.emit( + "runtime-process-stopped", + RuntimeStopped { + runtime_id: id, + status, + }, + ); + } + return; + } + }); +} + +fn parse_listening_url(line: &str) -> Option { + let rest = line.strip_prefix(LISTEN_PREFIX)?; + let raw_url = rest.split_whitespace().next()?; + let url = Url::parse(raw_url).ok()?; + if url.scheme() == "http" && url.host_str() == Some("127.0.0.1") { + Some(url) + } else { + None + } +} + +fn wait_for_listening( + child: &mut GroupChild, + listen_receiver: std::sync::mpsc::Receiver, + token: &str, + failure_output: &Mutex, +) -> Result { + let deadline = Instant::now() + STARTUP_TIMEOUT; + loop { + if let Some(status) = child + .try_wait() + .map_err(|error| format!("Failed to inspect bundled runtime: {error}"))? + { + return Err(startup_error( + &format!("Bundled runtime exited with status {status}."), + failure_output, + )); + } + match listen_receiver.recv_timeout(HEALTH_RETRY_INTERVAL) { + Ok(url) => return wait_for_health(child, url, token, deadline, failure_output), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + return Err(startup_error( + "Bundled runtime closed stdout before reporting its listening URL.", + failure_output, + )); + } + } + if Instant::now() >= deadline { + return Err(startup_error( + "Timed out waiting for the bundled runtime to listen.", + failure_output, + )); + } + } +} + +fn wait_for_health( + child: &mut GroupChild, + base_url: Url, + token: &str, + deadline: Instant, + failure_output: &Mutex, +) -> Result { + // `?deep=true` matters: the serve fast path answers a shallow `/health` + // from its bootstrap app before the real runtime (and the Web Shell) is + // mounted. Deep health stays 503 (`reason: "bootstrap"`) until the + // runtime app is ready, so navigating after a 200 cannot race into the + // deferred-runtime window. + let health_url = base_url + .join("health?deep=true") + .map_err(|error| format!("Failed to construct runtime health URL: {error}"))?; + let agent: ureq::Agent = ureq::Agent::config_builder() + .timeout_global(Some(HEALTH_REQUEST_TIMEOUT)) + .build() + .into(); + while Instant::now() < deadline { + if let Some(status) = child + .try_wait() + .map_err(|error| format!("Failed to inspect bundled runtime: {error}"))? + { + return Err(startup_error( + &format!("Bundled runtime exited with status {status}."), + failure_output, + )); + } + let response = agent + .get(health_url.as_str()) + .header("Authorization", &format!("Bearer {token}")) + .call(); + if response.is_ok_and(|response| { + response + .into_body() + .read_to_string() + .is_ok_and(|body| body.contains("\"status\":\"ok\"")) + }) { + return Ok(base_url); + } + thread::sleep(HEALTH_RETRY_INTERVAL); + } + Err(startup_error( + "Timed out waiting for the bundled runtime health check.", + failure_output, + )) +} + +fn startup_error(message: &str, output: &Mutex) -> String { + let output = match output.lock() { + Ok(guard) => guard.clone(), + Err(poisoned) => poisoned.into_inner().clone(), + }; + if output.trim().is_empty() { + message.to_string() + } else { + format!("{message}\n\nRuntime output:\n{}", output.trim()) + } +} + +fn runtime_arguments(workspace: &Path) -> Vec { + [ + OsString::from("serve"), + OsString::from("--port"), + OsString::from("0"), + OsString::from("--hostname"), + OsString::from("127.0.0.1"), + OsString::from("--require-auth"), + OsString::from("--workspace"), + workspace.as_os_str().to_owned(), + OsString::from("--no-open"), + ] + .into_iter() + .collect() +} + +#[cfg(test)] +mod tests { + use super::{ + append_failure_output, parse_listening_url, runtime_arguments, DesktopRuntime, + RuntimeStopped, FAILURE_OUTPUT_LIMIT, + }; + use std::path::Path; + use std::sync::Mutex; + use url::Url; + + #[test] + fn parses_loopback_listening_line() { + let url = parse_listening_url( + "qwen serve listening on http://127.0.0.1:49152 (mode=stdio, workspace=/tmp)", + ) + .expect("listening URL"); + assert_eq!(url.as_str(), "http://127.0.0.1:49152/"); + } + + #[test] + fn rejects_non_loopback_listening_line() { + assert!(parse_listening_url( + "qwen serve listening on http://0.0.0.0:4170 (mode=stdio, workspace=/tmp)" + ) + .is_none()); + } + + #[test] + fn carries_the_daemon_token_only_in_the_url_fragment() { + let runtime = DesktopRuntime { + id: 1, + base_url: Url::parse("http://127.0.0.1:49152/").expect("base URL"), + token: "secret-token".to_string(), + child: std::sync::Arc::new(std::sync::Mutex::new(None)), + stopping: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), + }; + assert_eq!( + runtime.authenticated_web_url().as_str(), + "http://127.0.0.1:49152/#token=secret-token" + ); + assert_eq!(runtime.base_url().as_str(), "http://127.0.0.1:49152/"); + } + + #[test] + fn runtime_arguments_enable_ephemeral_authenticated_web_shell() { + let args = runtime_arguments(Path::new("/tmp/workspace")); + let args: Vec<_> = args + .iter() + .map(|value| value.to_string_lossy().into_owned()) + .collect(); + assert_eq!( + args, + [ + "serve", + "--port", + "0", + "--hostname", + "127.0.0.1", + "--require-auth", + "--workspace", + "/tmp/workspace", + "--no-open", + ] + ); + } + + #[test] + fn runtime_stopped_payload_uses_camel_case_runtime_id() { + let payload = RuntimeStopped { + runtime_id: 7, + status: "exited".to_string(), + }; + + assert_eq!( + serde_json::to_value(payload).expect("payload"), + serde_json::json!({ "runtimeId": 7, "status": "exited" }), + ); + } + + #[test] + fn failure_output_limit_respects_utf8_boundaries() { + let output = Mutex::new("a".repeat(FAILURE_OUTPUT_LIMIT - 1)); + append_failure_output(&output, "中"); + assert_eq!( + output.lock().expect("output").len(), + FAILURE_OUTPUT_LIMIT - 1 + ); + } +} diff --git a/packages/desktop-shell/src-tauri/tauri.conf.json b/packages/desktop-shell/src-tauri/tauri.conf.json new file mode 100644 index 00000000000..bd33d82601f --- /dev/null +++ b/packages/desktop-shell/src-tauri/tauri.conf.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Qwen Code", + "version": "0.0.1", + "identifier": "com.qwen.code.desktop", + "build": { + "frontendDist": "../bootstrap" + }, + "app": { + "withGlobalTauri": true, + "windows": [], + "security": { + "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src ipc: http://ipc.localhost; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'", + "capabilities": ["bootstrap"] + } + }, + "bundle": { + "active": true, + "targets": ["app", "dmg", "nsis", "appimage", "deb"], + "createUpdaterArtifacts": true, + "category": "DeveloperTool", + "shortDescription": "Qwen Code desktop shell for the existing Web Shell", + "icon": [ + "icons/icon.png", + "icons/icon.icns", + "icons/icon.ico", + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png" + ], + "resources": { + "../runtime/qwen-code": "runtime/qwen-code" + }, + "macOS": { + "hardenedRuntime": true, + "entitlements": "Entitlements.plist", + "minimumSystemVersion": "11.0" + }, + "linux": { + "appimage": { + "bundleMediaFramework": false + }, + "deb": {} + }, + "windows": { + "digestAlgorithm": "sha256", + "timestampUrl": "http://timestamp.digicert.com", + "webviewInstallMode": { + "type": "downloadBootstrapper", + "silent": true + }, + "nsis": { + "installMode": "currentUser", + "installerIcon": "icons/icon.ico" + } + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENEREE1N0EwMkY5OUNDMzAKUldRd3pKa3ZvRmZhemRMb3Y0M3gzMytEZEtQZk5tWlNWWWhqTWM0ZUxiTHNzSHA4SUZJOFV5cnAK", + "endpoints": [ + "https://github.com/QwenLM/qwen-code/releases/download/desktop-latest/desktop-latest.json" + ] + } + } +} diff --git a/packages/desktop-shell/src-tauri/windows-app-manifest.xml b/packages/desktop-shell/src-tauri/windows-app-manifest.xml new file mode 100644 index 00000000000..90010f53bef --- /dev/null +++ b/packages/desktop-shell/src-tauri/windows-app-manifest.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + true + + + diff --git a/scripts/check-desktop-isolation.js b/scripts/check-desktop-isolation.js index 75977db2deb..cf15704b41c 100644 --- a/scripts/check-desktop-isolation.js +++ b/scripts/check-desktop-isolation.js @@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = join(__dirname, '..'); -const desktopPrefix = 'packages/desktop'; +const desktopPrefixes = ['packages/desktop', 'packages/desktop-shell']; const forbiddenRootPackages = [ 'electron', 'electron-builder', @@ -25,7 +25,9 @@ let hasError = false; console.log('Checking desktop workspace isolation...'); function isDesktopLocation(location) { - return location === desktopPrefix || location.startsWith(`${desktopPrefix}/`); + return desktopPrefixes.some( + (prefix) => location === prefix || location.startsWith(`${prefix}/`), + ); } function reportError(message, values = []) {