From 9a74a78c711607e8eac40ff5e1a4c6038613a053 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Fri, 9 Oct 2026 12:57:12 -0300 Subject: [PATCH] ci: only run what a change affects, consolidate jobs (#3674) * ci: only run what a change affects, consolidate jobs - detect affected plugins from changed files and Cargo.lock dependency trees - test/lint Rust per platform in shards instead of one job per plugin - fold check-generated-files into lint-javascript, fmt into a single job - skip fat LTO in integration test builds, single ABI in test-android * ci: run e2e on bumps, use published tauri CLI, PR-safe pnpm cache - trigger e2e on Cargo/pnpm lockfile and manifest changes - install the latest 2.x tauri-cli with cargo-binstall in integration tests - add setup-pnpm action: restore the store everywhere, save only on v2/v3 * fix clippy, ios build * fix bumps [skip ci] --- .changes/ios-wry-feature.md | 8 + .github/actions/setup-pnpm/action.yml | 44 ++++ .github/workflows/audit-javascript.yml | 8 +- .github/workflows/check-generated-files.yml | 175 ------------- .github/workflows/check-license-header.yml | 2 +- .github/workflows/fmt.yml | 67 +++-- .github/workflows/integration-tests.yml | 81 +++++- .github/workflows/lint-javascript.yml | 39 +-- .github/workflows/lint-rust.yml | 184 +++++--------- .github/workflows/sync.yml | 9 +- .github/workflows/test-android.yml | 19 +- .github/workflows/test-api-e2e-mobile.yml | 41 +-- .github/workflows/test-api-e2e.yml | 28 +-- .github/workflows/test-rust.yml | 263 +++++--------------- .scripts/ci/affected-rust-packages.js | 254 +++++++++++++++++++ plugins/autostart/src/lib.rs | 2 +- plugins/biometric/Cargo.toml | 3 + plugins/nfc/Cargo.toml | 3 + 18 files changed, 600 insertions(+), 630 deletions(-) create mode 100644 .changes/ios-wry-feature.md create mode 100644 .github/actions/setup-pnpm/action.yml delete mode 100644 .github/workflows/check-generated-files.yml create mode 100644 .scripts/ci/affected-rust-packages.js diff --git a/.changes/ios-wry-feature.md b/.changes/ios-wry-feature.md new file mode 100644 index 000000000..db7155c1b --- /dev/null +++ b/.changes/ios-wry-feature.md @@ -0,0 +1,8 @@ +--- +biometric: patch +nfc: patch +biometric-js: patch +nfc-js: patch +--- + +Fix compilation on iOS when the `wry` feature of `tauri` is not enabled by another crate. diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml new file mode 100644 index 000000000..a71b9cb39 --- /dev/null +++ b/.github/actions/setup-pnpm/action.yml @@ -0,0 +1,44 @@ +# Copyright 2019-2023 Tauri Programme within The Commons Conservancy +# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: MIT + +name: setup pnpm +description: Installs Node.js, pnpm and the workspace dependencies, restoring the pnpm store from the cache + +# `pnpm/action-setup`'s own cache saves from every run, pull requests included. An entry saved from +# a pull request lives on its merge ref, where nothing else can restore it, and every write evicts +# an entry that other runs would have hit (10 GB repository limit, LRU eviction), so here every run +# restores the store and only `v2`/`v3` save it. +runs: + using: composite + steps: + - uses: actions/setup-node@v7 + with: + node-version: 'lts/*' + + - uses: pnpm/action-setup@v6 + + - id: store + shell: bash + run: echo "path=$(pnpm store path --silent)" >> "$GITHUB_OUTPUT" + + - id: cache + uses: actions/cache/restore@v6 + with: + path: ${{ steps.store.outputs.path }} + key: pnpm-store-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pnpm-lock.yaml') }} + restore-keys: pnpm-store-${{ runner.os }}-${{ runner.arch }}- + + - shell: bash + run: pnpm install --frozen-lockfile + + # a store restored from an older lockfile still holds the packages it no longer uses + - if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && steps.cache.outputs.cache-hit != 'true' }} + shell: bash + run: pnpm store prune + + - if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && steps.cache.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: ${{ steps.store.outputs.path }} + key: ${{ steps.cache.outputs.cache-primary-key }} diff --git a/.github/workflows/audit-javascript.yml b/.github/workflows/audit-javascript.yml index 151d913a2..9536dfb88 100644 --- a/.github/workflows/audit-javascript.yml +++ b/.github/workflows/audit-javascript.yml @@ -38,12 +38,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - run_install: true - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm - name: audit run: pnpm audit diff --git a/.github/workflows/check-generated-files.yml b/.github/workflows/check-generated-files.yml deleted file mode 100644 index 11f40d1b1..000000000 --- a/.github/workflows/check-generated-files.yml +++ /dev/null @@ -1,175 +0,0 @@ -# Copyright 2019-2023 Tauri Programme within The Commons Conservancy -# SPDX-License-Identifier: Apache-2.0 -# SPDX-License-Identifier: MIT - -name: check generated files - -on: - pull_request: - paths: - - '.github/workflows/check-generated-files.yml' - - pnpm-lock.yaml - - '**/guest-js/**' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - changes: - runs-on: ubuntu-latest - outputs: - packages: ${{ steps.filter.outputs.changes }} - steps: - - uses: actions/checkout@v7 - - uses: dorny/paths-filter@v2 - id: filter - with: - filters: | - autostart: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/autostart/guest-js/** - - plugins/autostart/src/api-iife.js - cli: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/cli/guest-js/** - - plugins/cli/src/api-iife.js - clipboard-manager: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/clipboard-manager/guest-js/** - - plugins/clipboard-manager/src/api-iife.js - dialog: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/dialog/guest-js/** - - plugins/dialog/src/api-iife.js - fs: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/fs/guest-js/** - - plugins/fs/src/api-iife.js - geolocation: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/geolocation/guest-js/** - - plugins/geolocation/src/api-iife.js - global-shortcut: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/global-shortcut/guest-js/** - - plugins/global-shortcut/src/api-iife.js - opener: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/opener/guest-js/** - - plugins/opener/src/api-iife.js - haptics: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/haptics/guest-js/** - - plugins/haptics/src/api-iife.js - http: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/http/guest-js/** - - plugins/http/src/api-iife.js - log: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/log/guest-js/** - - plugins/log/src/api-iife.js - notification: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/notification/guest-js/** - - plugins/notification/src/api-iife.js - os: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/os/guest-js/** - - plugins/os/src/api-iife.js - positioner: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/positioner/guest-js/** - - plugins/positioner/src/api-iife.js - process: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/process/guest-js/** - - plugins/process/src/api-iife.js - shell: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/shell/guest-js/** - - plugins/shell/src/api-iife.js - sql: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/sql/guest-js/** - - plugins/sql/src/api-iife.js - store: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/store/guest-js/** - - plugins/store/src/api-iife.js - stronghold: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/stronghold/guest-js/** - - plugins/stronghold/src/api-iife.js - updater: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/updater/guest-js/** - - plugins/updater/src/api-iife.js - upload: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/upload/guest-js/** - - plugins/upload/src/api-iife.js - websocket: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/websocket/guest-js/** - - plugins/websocket/src/api-iife.js - window-state: - - .github/workflows/check-generated-files.yml - - pnpm-lock.yaml - - plugins/window-state/guest-js/** - - plugins/window-state/src/api-iife.js - - test: - needs: changes - if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} - strategy: - fail-fast: false - matrix: - package: ${{ fromJSON(needs.changes.outputs.packages) }} - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - run_install: true - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - - - name: build api - working-directory: plugins/${{ matrix.package }} - run: pnpm install && pnpm build - - - name: check diff - run: | - ./.scripts/ci/has-diff.sh diff --git a/.github/workflows/check-license-header.yml b/.github/workflows/check-license-header.yml index 0100d13f9..30f366c77 100644 --- a/.github/workflows/check-license-header.yml +++ b/.github/workflows/check-license-header.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: dorny/paths-filter@v2 + - uses: dorny/paths-filter@v3 id: filter with: list-files: shell diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index bc23cdcef..0db8e0733 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -7,49 +7,64 @@ name: check formatting on: pull_request: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: - rustfmt: + # A single job whose checks only run when files they cover changed: the checks themselves take + # seconds, so a runner each would mostly be spent waiting for and setting up runners. + fmt: runs-on: ubuntu-latest - + permissions: + contents: read + pull-requests: read # to list the changed files steps: - uses: actions/checkout@v7 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + rust: + - .github/workflows/fmt.yml + - '**/*.rs' + prettier: + - .github/workflows/fmt.yml + - .prettierrc + - .prettierignore + - pnpm-lock.yaml + - '**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx,json,json5,jsonc,md,mdx,yml,yaml,css,scss,less,html,vue,svelte}' + toml: + - .github/workflows/fmt.yml + - '**/*.toml' + - name: install Rust stable and rustfmt + if: steps.filter.outputs.rust == 'true' uses: dtolnay/rust-toolchain@stable with: components: rustfmt - - name: run cargo fmt + - name: rustfmt + if: steps.filter.outputs.rust == 'true' run: cargo fmt --all -- --check - prettier: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - run_install: true - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow - - run: pnpm format:check - - taplo: - name: taplo (.toml files) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - - name: install Rust stable - uses: dtolnay/rust-toolchain@stable - - name: install taplo-cli + if: ${{ !cancelled() && steps.filter.outputs.toml == 'true' }} uses: taiki-e/install-action@v2 with: tool: taplo-cli - - run: taplo fmt --check --diff + - name: taplo (.toml files) + if: ${{ !cancelled() && steps.filter.outputs.toml == 'true' }} + run: taplo fmt --check --diff + + - uses: ./.github/actions/setup-pnpm + if: ${{ !cancelled() && steps.filter.outputs.prettier == 'true' }} + + - name: prettier + if: ${{ !cancelled() && steps.filter.outputs.prettier == 'true' }} + run: pnpm format:check diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 7d2bb9385..92dc999d0 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -8,22 +8,36 @@ on: workflow_dispatch: # run before publishing, see covector-version-or-publish.yml workflow_call: + inputs: + all: + description: Run even if the updater is not affected by the pushed changes + type: boolean + default: true push: branches: - v1 - v2 - paths: + paths: &paths - '.github/workflows/integration-tests.yml' - - 'plugins/updater/src/**' - - 'plugins/updater/tests/**' + - '.scripts/ci/affected-rust-packages.js' + - 'plugins/updater/**' + - '!plugins/updater/guest-js/**' + - '!plugins/updater/*.md' + - '!plugins/updater/package.json' + - '!plugins/updater/api-iife.js' + - 'Cargo.toml' + - 'Cargo.lock' pull_request: branches: - v1 - v2 - paths: - - '.github/workflows/integration-tests.yml' - - 'plugins/updater/src/**' - - 'plugins/updater/tests/**' + paths: *paths + +concurrency: + # `github.workflow` is the caller's name in a `workflow_call` run, so the suffix keeps this + # workflow from sharing (and cancelling) the group of the other workflows the caller runs. + group: ${{ github.workflow }}-integration-tests-${{ github.ref }} + cancel-in-progress: true permissions: contents: read @@ -32,8 +46,30 @@ env: CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache jobs: + # A `Cargo.lock` change only runs the tests when it touches the updater's dependency tree; see + # .scripts/ci/affected-rust-packages.js. + changes: + runs-on: ubuntu-latest + outputs: + updater: ${{ contains(fromJSON(steps.affected.outputs.packages), 'tauri-plugin-updater') }} + steps: + - uses: actions/checkout@v7 + with: + # a pull request checks out its merge commit, whose first parent is the base branch + fetch-depth: 2 + - id: affected + run: node .scripts/ci/affected-rust-packages.js + env: + BASE: ${{ github.event_name == 'pull_request' && 'HEAD^1' || github.event.before }} + ALL: ${{ inputs.all == true || github.event_name == 'workflow_dispatch' }} + GLOBAL_PATHS: .github/workflows/integration-tests.yml + run-integration-tests: + needs: changes + if: needs.changes.outputs.updater == 'true' runs-on: ${{ matrix.platform }} + # a stuck step (e.g. `apt-get` waiting on an unreachable mirror) otherwise runs for 6 hours + timeout-minutes: 90 strategy: fail-fast: false @@ -61,12 +97,27 @@ jobs: # that other runs would have hit (10 GB repository limit, LRU eviction). save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} + - uses: taiki-e/install-action@v2 + with: + tool: cargo-binstall + + # The latest published 2.x CLI, downloaded from its GitHub release. `--pkg-fmt` because the + # crate's binstall metadata expects a .tgz on aarch64-apple-darwin, where the release only + # ships a .zip, and the disabled strategies make a missing binary fail the step instead of + # silently compiling the CLI from source. The token authenticates the GitHub requests and the + # longer timeout and retries ride out a slow response, which binstall otherwise reports as a + # missing binary. - name: install Tauri CLI - # `--debug --target-dir target`: a dev-profile build in the workspace target dir reuses - # the cached dependencies (shared with the test build below) instead of a from-scratch - # release build in a temporary dir. `--locked` keeps the dependency versions stable - # between runs, so the cached artifacts stay valid. - run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch dev --locked --debug --target-dir target + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + for attempt in 1 2 3; do + cargo binstall -y tauri-cli@^2 --pkg-fmt ${{ runner.os == 'Linux' && 'tgz' || 'zip' }} --disable-strategies quick-install,compile --maximum-resolution-timeout 60 && exit 0 + echo "::warning::cargo binstall failed (attempt $attempt)" + sleep 10 + done + exit 1 - name: grant admin rights without a prompt if: startsWith(matrix.platform, 'macos') @@ -80,4 +131,10 @@ jobs: echo "TAURI_UPDATER_ADMIN_PROMPT_TEST=1" >> "$GITHUB_ENV" - name: run integration tests + env: + # The tests bundle the app with `cargo tauri build` over and over, changing nothing but + # its version, and the workspace release profile (fat LTO, a single codegen unit) spends + # about a minute linking each of those builds. None of it matters to the updater. + CARGO_PROFILE_RELEASE_LTO: 'false' + CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16' run: cargo test --test '*' -- --ignored diff --git a/.github/workflows/lint-javascript.yml b/.github/workflows/lint-javascript.yml index c6dbce4d4..e21e6672b 100644 --- a/.github/workflows/lint-javascript.yml +++ b/.github/workflows/lint-javascript.yml @@ -9,26 +9,27 @@ on: branches: - v1 - v2 - paths: + paths: &paths - '.github/workflows/lint-javascript.yml' + - '.github/actions/setup-pnpm/**' - 'plugins/*/guest-js/**' + # generated by `pnpm build`, checked below + - 'plugins/*/api-iife.js' + - 'plugins/*/src/init-iife.js' + - 'plugins/*/rollup.config.js' + - 'plugins/*/tsconfig.json' + - 'shared/**' - 'packages/**' - - '.eslintignore' - - '.eslintrc.json' - - '.prettierignore' + - 'eslint.config.js' + - 'tsconfig.base.json' + - 'pnpm-lock.yaml' + - 'pnpm-workspace.yaml' - '**/package.json' pull_request: branches: - v1 - v2 - paths: - - '.github/workflows/lint-javascript.yml' - - 'plugins/*/guest-js/**' - - 'packages/**' - - '.eslintignore' - - '.eslintrc.json' - - '.prettierignore' - - '**/package.json' + paths: *paths concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -42,18 +43,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - run_install: true - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm # the type-checked lint rules (and packages/api-e2e) resolve the plugins' # types from their dist-js build output - name: build + id: build run: pnpm build - name: eslint run: pnpm lint - name: ts:check run: pnpm ts:check + # the build above regenerates the `api-iife.js` and `init-iife.js` files the Rust crates + # embed, which must be committed along with the guest-js changes they come from + - name: check generated files are up to date + if: ${{ !cancelled() && steps.build.outcome == 'success' }} + run: ./.scripts/ci/has-diff.sh diff --git a/.github/workflows/lint-rust.yml b/.github/workflows/lint-rust.yml index b2c5ca99d..800966693 100644 --- a/.github/workflows/lint-rust.yml +++ b/.github/workflows/lint-rust.yml @@ -9,20 +9,23 @@ on: branches: - v1 - v2 - paths: + paths: &paths - '.github/workflows/lint-rust.yml' - - 'plugins/*/src/**' - - '!plugins/*/src/api-iife.js' - - '**/Cargo.toml' + - '.scripts/ci/affected-rust-packages.js' + - 'plugins/**' + - '!plugins/*/guest-js/**' + - '!plugins/*/android/**' + - '!plugins/*/*.md' + - '!plugins/*/package.json' + - '!plugins/*/api-iife.js' + - 'Cargo.toml' + - 'Cargo.lock' pull_request: branches: - v1 - v2 - paths: - - '.github/workflows/lint-rust.yml' - - 'plugins/*/src/**' - - '!plugins/*/src/api-iife.js' - - '**/Cargo.toml' + paths: *paths + workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -32,128 +35,35 @@ permissions: contents: read jobs: + # Lists the plugins the change affects and splits them in shards, linted one after the other in + # a single job per platform; see test-rust.yml for the reasoning. changes: runs-on: ubuntu-latest - permissions: - pull-requests: read outputs: - packages: ${{ steps.filter.outputs.changes }} - saver: ${{ steps.saver.outputs.saver }} + shards: ${{ steps.affected.outputs.shards }} steps: - uses: actions/checkout@v7 - - uses: dorny/paths-filter@v2 - id: filter with: - filters: | - tauri-plugin-autostart: - - .github/workflows/lint-rust.yml - - plugins/autostart/** - tauri-plugin-cli: - - .github/workflows/lint-rust.yml - - plugins/cli/** - tauri-plugin-clipboard-manager: - - .github/workflows/lint-rust.yml - - plugins/clipboard-manager/** - tauri-plugin-deep-link: - - .github/workflows/lint-rust.yml - - plugins/deep-link/** - tauri-plugin-dialog: - - .github/workflows/lint-rust.yml - - plugins/dialog/** - - plugins/fs/** - tauri-plugin-fs: - - .github/workflows/lint-rust.yml - - plugins/fs/** - tauri-plugin-geolocation: - - .github/workflows/lint-rust.yml - - plugins/geolocation/** - tauri-plugin-global-shortcut: - - .github/workflows/lint-rust.yml - - plugins/global-shortcut/** - tauri-plugin-opener: - - .github/workflows/lint-rust.yml - - plugins/opener/** - tauri-plugin-haptics: - - .github/workflows/lint-rust.yml - - plugins/haptics/** - tauri-plugin-http: - - .github/workflows/lint-rust.yml - - plugins/http/** - - plugins/fs/** - tauri-plugin-localhost: - - .github/workflows/lint-rust.yml - - plugins/localhost/** - tauri-plugin-log: - - .github/workflows/lint-rust.yml - - plugins/log/** - tauri-plugin-notification: - - .github/workflows/lint-rust.yml - - plugins/notification/** - tauri-plugin-os: - - .github/workflows/lint-rust.yml - - plugins/os/** - tauri-plugin-persisted-scope: - - .github/workflows/lint-rust.yml - - plugins/persisted-scope/** - - plugins/fs/** - tauri-plugin-positioner: - - .github/workflows/lint-rust.yml - - plugins/positioner/** - tauri-plugin-process: - - .github/workflows/lint-rust.yml - - plugins/process/** - tauri-plugin-shell: - - .github/workflows/lint-rust.yml - - plugins/shell/** - tauri-plugin-single-instance: - - .github/workflows/lint-rust.yml - - plugins/single-instance/** - tauri-plugin-sql: - - .github/workflows/lint-rust.yml - - plugins/sql/** - tauri-plugin-store: - - .github/workflows/lint-rust.yml - - plugins/store/** - tauri-plugin-stronghold: - - .github/workflows/lint-rust.yml - - plugins/stronghold/** - tauri-plugin-updater: - - .github/workflows/lint-rust.yml - - plugins/updater/** - tauri-plugin-upload: - - .github/workflows/lint-rust.yml - - plugins/upload/** - tauri-plugin-websocket: - - .github/workflows/lint-rust.yml - - plugins/websocket/** - tauri-plugin-window-state: - - .github/workflows/lint-rust.yml - - plugins/window-state/** - - # Every package job for a platform restores the same rust-cache entry, and exactly one - # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose - # dependency tree covers most of the other plugins' dependencies, so the entry is as - # useful as possible for the packages that did not save it. - - name: pick the package that saves the cache - id: saver + # a pull request checks out its merge commit, whose first parent is the base branch + fetch-depth: 2 + - id: affected + run: node .scripts/ci/affected-rust-packages.js env: - PACKAGES: ${{ steps.filter.outputs.changes }} - run: | - saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' - ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] - | map(select(. as $p | $packages | index($p) != null)) - | first // $packages[0] // empty - ')" - echo "saver=$saver" >> "$GITHUB_OUTPUT" + BASE: ${{ github.event_name == 'pull_request' && 'HEAD^1' || github.event.before }} + ALL: ${{ github.event_name == 'workflow_dispatch' }} + GLOBAL_PATHS: .github/workflows/lint-rust.yml clippy: + name: clippy (${{ matrix.platform.target }}, ${{ matrix.shard.name }}) needs: changes - if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} + if: ${{ needs.changes.outputs.shards != '[]' && needs.changes.outputs.shards != '' }} runs-on: ${{ matrix.platform.os }} + # a stuck step (e.g. `apt-get` waiting on an unreachable mirror) otherwise runs for 6 hours + timeout-minutes: 90 strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.changes.outputs.packages) }} + shard: ${{ fromJSON(needs.changes.outputs.shards) }} platform: - target: x86_64-pc-windows-msvc os: windows-latest @@ -194,14 +104,36 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - # One entry shared by every package job (the default key is per job, not per - # matrix entry), only written from `v2`/`v3` and by a single package job; see - # test-rust.yml for the reasoning. - save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} + # One entry per target shared by every shard (the default key is per job and runner + # OS, which the Linux and Android jobs, and the macOS and iOS ones, would share), only + # written from `v2`/`v3` and by a single shard; see test-rust.yml for the reasoning. + key: ${{ matrix.platform.target }} + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.shard.save }} - - name: clippy ${{ matrix.package }} - run: ${{ matrix.platform.cargo }} clippy --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets -- -D warnings + # The shard's packages are linted one after the other, and a failing package does not stop + # the others: the step fails at the end if any did. + - name: clippy ${{ matrix.shard.name }} + shell: bash + env: + PACKAGES: ${{ matrix.shard.packages }} + CARGO: ${{ matrix.platform.cargo }} + TARGET: ${{ matrix.platform.target }} + run: | + failed=() + for package in $PACKAGES; do + echo "::group::clippy $package" + if ! $CARGO clippy --package "$package" --target "$TARGET" --all-targets -- -D warnings; then + failed+=("$package") + # the features of `dialog` cannot all be enabled at once + elif [ "$package" != tauri-plugin-dialog ] && ! $CARGO clippy --package "$package" --target "$TARGET" --all-targets --all-features -- -D warnings; then + failed+=("$package (--all-features)") + fi + echo "::endgroup::" + done - - name: clippy ${{ matrix.package }} --all-features - if: matrix.package != 'tauri-plugin-dialog' - run: ${{ matrix.platform.cargo }} clippy --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --all-features -- -D warnings + if [ ${#failed[@]} -gt 0 ]; then + for package in "${failed[@]}"; do + echo "::error title=clippy failed::$package" + done + exit 1 + fi diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 002f5bdd4..57048de80 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -27,14 +27,7 @@ jobs: - name: Fetch git tags run: git fetch origin 'refs/tags/*:refs/tags/*' - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - - uses: pnpm/action-setup@v6 - with: - run_install: true - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm - name: Build packages run: pnpm build diff --git a/.github/workflows/test-android.yml b/.github/workflows/test-android.yml index 023d003bf..12499ed6a 100644 --- a/.github/workflows/test-android.yml +++ b/.github/workflows/test-android.yml @@ -27,21 +27,16 @@ jobs: matrix: platform: [ubuntu-latest, macos-latest, windows-latest] - # No caches on purpose: this workflow only runs on pull requests, so anything it saved - # would live on the merge ref where no other run can restore it, while still counting - # against the repository's 10 GB cache limit and evicting entries that `v2`/`v3` saved. + # Nothing is saved to the cache on purpose: this workflow only runs on pull requests, so + # anything it saved would live on the merge ref where no other run can restore it, while + # still counting against the repository's 10 GB cache limit and evicting entries that + # `v2`/`v3` saved. Restoring is fine, as setup-pnpm does with the pnpm store. steps: - uses: actions/checkout@v7 - uses: dtolnay/rust-toolchain@1.90 - - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - - uses: pnpm/action-setup@v6 - with: - run_install: true + - uses: ./.github/actions/setup-pnpm - uses: actions/setup-java@v5 with: @@ -71,8 +66,10 @@ jobs: - name: build API run: pnpm build + # A release build, so R8 runs with the plugins' ProGuard rules, of a single ABI: the + # other three would only repeat the (uncached) release build of the Rust code. - name: build APK working-directory: ./examples/api - run: pnpm tauri android build + run: pnpm tauri android build --target aarch64 env: NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} diff --git a/.github/workflows/test-api-e2e-mobile.yml b/.github/workflows/test-api-e2e-mobile.yml index 3b577013c..f293d9ca2 100644 --- a/.github/workflows/test-api-e2e-mobile.yml +++ b/.github/workflows/test-api-e2e-mobile.yml @@ -11,7 +11,7 @@ on: push: branches: - v2 - paths: + paths: &paths - '.github/workflows/test-api-e2e-mobile.yml' - 'packages/api-e2e/**' - 'examples/api/**' @@ -21,19 +21,16 @@ on: - 'plugins/*/ios/**' - 'plugins/*/permissions/**' - 'plugins/*/build.rs' + # dependency bumps + - 'Cargo.toml' + - 'Cargo.lock' + - 'plugins/*/Cargo.toml' + - 'pnpm-lock.yaml' + - '**/package.json' pull_request: branches: - v2 - paths: - - '.github/workflows/test-api-e2e-mobile.yml' - - 'packages/api-e2e/**' - - 'examples/api/**' - - 'plugins/*/guest-js/**' - - 'plugins/*/src/**' - - 'plugins/*/android/**' - - 'plugins/*/ios/**' - - 'plugins/*/permissions/**' - - 'plugins/*/build.rs' + paths: *paths env: RUST_BACKTRACE: 1 @@ -63,13 +60,7 @@ jobs: with: targets: x86_64-linux-android - - name: setup node - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm - uses: actions/setup-java@v5 with: @@ -122,9 +113,6 @@ jobs: # that other runs would have hit (10 GB repository limit, LRU eviction). save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} - - name: install dependencies - run: pnpm i --frozen-lockfile - - name: build plugins run: pnpm build @@ -186,13 +174,7 @@ jobs: with: targets: aarch64-apple-ios-sim - - name: setup node - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm - uses: Swatinem/rust-cache@v2 with: @@ -202,9 +184,6 @@ jobs: # that other runs would have hit (10 GB repository limit, LRU eviction). save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} - - name: install dependencies - run: pnpm i --frozen-lockfile - - name: build plugins run: pnpm build diff --git a/.github/workflows/test-api-e2e.yml b/.github/workflows/test-api-e2e.yml index 248997345..fa50829a5 100644 --- a/.github/workflows/test-api-e2e.yml +++ b/.github/workflows/test-api-e2e.yml @@ -11,7 +11,7 @@ on: push: branches: - v2 - paths: + paths: &paths - '.github/workflows/test-api-e2e.yml' - 'packages/api-e2e/**' - 'examples/api/**' @@ -19,17 +19,16 @@ on: - 'plugins/*/src/**' - 'plugins/*/permissions/**' - 'plugins/*/build.rs' + # dependency bumps + - 'Cargo.toml' + - 'Cargo.lock' + - 'plugins/*/Cargo.toml' + - 'pnpm-lock.yaml' + - '**/package.json' pull_request: branches: - v2 - paths: - - '.github/workflows/test-api-e2e.yml' - - 'packages/api-e2e/**' - - 'examples/api/**' - - 'plugins/*/guest-js/**' - - 'plugins/*/src/**' - - 'plugins/*/permissions/**' - - 'plugins/*/build.rs' + paths: *paths env: RUST_BACKTRACE: 1 @@ -63,13 +62,7 @@ jobs: - name: install Rust stable uses: dtolnay/rust-toolchain@stable - - name: setup node - uses: actions/setup-node@v7 - with: - node-version: 'lts/*' - - uses: pnpm/action-setup@v6 - with: - cache: true # the pnpm store, keyed by OS and lockfile hash and shared by every workflow + - uses: ./.github/actions/setup-pnpm - name: install Linux dependencies if: runner.os == 'Linux' @@ -117,9 +110,6 @@ jobs: # that other runs would have hit (10 GB repository limit, LRU eviction). save-if: ${{ github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3' }} - - name: install dependencies - run: pnpm i --frozen-lockfile - - name: build plugins run: pnpm build diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index 6c942198a..97a6b825e 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -9,22 +9,23 @@ on: branches: - v1 - v2 - paths: + paths: &paths - '.github/workflows/test-rust.yml' - - 'plugins/*/src/**' - - '!plugins/*/src/api-iife.js' - - '**/Cargo.toml' - - '**/Cargo.lock' + - '.scripts/ci/affected-rust-packages.js' + - 'plugins/**' + - '!plugins/*/guest-js/**' + - '!plugins/*/android/**' + - '!plugins/*/*.md' + - '!plugins/*/package.json' + - '!plugins/*/api-iife.js' + - 'Cargo.toml' + - 'Cargo.lock' pull_request: branches: - v1 - v2 - paths: - - '.github/workflows/test-rust.yml' - - 'plugins/*/src/**' - - '!plugins/*/src/api-iife.js' - - '**/Cargo.toml' - - '**/Cargo.lock' + paths: *paths + workflow_dispatch: env: CARGO_PROFILE_DEV_DEBUG: 0 # no debuginfo: faster builds and a much smaller target dir to cache @@ -38,182 +39,35 @@ permissions: contents: read jobs: + # Lists the plugins the change affects (see .scripts/ci/affected-rust-packages.js) and splits + # them in shards. Each shard is tested in a single job per platform, one package after the other + # in the same target dir, so the dependencies the plugins share (tauri, wry, tao...) are built + # (or restored) once per job instead of once per package, and a change that affects every plugin + # needs a few jobs per platform instead of one per plugin. changes: runs-on: ubuntu-latest - permissions: - pull-requests: read outputs: - packages: ${{ steps.filter.outputs.changes }} - saver: ${{ steps.saver.outputs.saver }} + shards: ${{ steps.affected.outputs.shards }} steps: - uses: actions/checkout@v7 - - uses: dorny/paths-filter@v2 - id: filter with: - base: v2 - filters: | - tauri-plugin-autostart: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/autostart/** - tauri-plugin-cli: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/cli/** - tauri-plugin-clipboard-manager: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/clipboard-manager/** - tauri-plugin-deep-link: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/deep-link/** - tauri-plugin-dialog: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/dialog/** - - plugins/fs/** - tauri-plugin-fs: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/fs/** - tauri-plugin-geolocation: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/geolocation/** - tauri-plugin-global-shortcut: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/global-shortcut/** - tauri-plugin-opener: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/opener/** - tauri-plugin-haptics: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/haptics/** - tauri-plugin-http: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/http/** - - plugins/fs/** - tauri-plugin-localhost: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/localhost/** - tauri-plugin-log: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/log/** - tauri-plugin-notification: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/notification/** - tauri-plugin-os: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/os/** - tauri-plugin-persisted-scope: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/persisted-scope/** - - plugins/fs/** - tauri-plugin-positioner: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/positioner/** - tauri-plugin-process: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/process/** - tauri-plugin-shell: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/shell/** - tauri-plugin-single-instance: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/single-instance/** - tauri-plugin-sql: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/sql/** - tauri-plugin-store: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/store/** - tauri-plugin-stronghold: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/stronghold/** - tauri-plugin-updater: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/updater/** - tauri-plugin-upload: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/upload/** - tauri-plugin-websocket: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/websocket/** - tauri-plugin-window-state: - - .github/workflows/test-rust.yml - - Cargo.toml - - Cargo.lock - - plugins/window-state/** - - # Every package job for a platform restores the same rust-cache entry, and exactly one - # of them saves it (see the `save-if` of the rust-cache step). Prefer the packages whose - # dependency tree covers most of the other plugins' dependencies, so the entry is as - # useful as possible for the packages that did not save it. - - name: pick the package that saves the cache - id: saver + # a pull request checks out its merge commit, whose first parent is the base branch + fetch-depth: 2 + - id: affected + run: node .scripts/ci/affected-rust-packages.js env: - PACKAGES: ${{ steps.filter.outputs.changes }} - run: | - saver="$(jq -rn --argjson packages "${PACKAGES:-[]}" ' - ["tauri-plugin-upload", "tauri-plugin-sql", "tauri-plugin-updater", "tauri-plugin-single-instance"] - | map(select(. as $p | $packages | index($p) != null)) - | first // $packages[0] // empty - ')" - echo "saver=$saver" >> "$GITHUB_OUTPUT" + BASE: ${{ github.event_name == 'pull_request' && 'HEAD^1' || github.event.before }} + ALL: ${{ github.event_name == 'workflow_dispatch' }} + GLOBAL_PATHS: .github/workflows/test-rust.yml test: + name: test (${{ matrix.platform.target }}, ${{ matrix.shard.name }}) needs: changes - if: ${{ needs.changes.outputs.packages != '[]' && needs.changes.outputs.packages != '' }} + if: ${{ needs.changes.outputs.shards != '[]' && needs.changes.outputs.shards != '' }} strategy: fail-fast: false matrix: - package: ${{ fromJSON(needs.changes.outputs.packages) }} + shard: ${{ fromJSON(needs.changes.outputs.shards) }} platform: - { target: x86_64-pc-windows-msvc, @@ -247,6 +101,8 @@ jobs: } runs-on: ${{ matrix.platform.os }} + # a stuck step (e.g. `apt-get` waiting on an unreachable mirror) otherwise runs for 6 hours + timeout-minutes: 90 steps: - uses: actions/checkout@v7 @@ -263,15 +119,16 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - # One entry per platform, shared by every package job: the plugins share most of - # their dependency tree (tauri, wry, tao...), and an entry per package and platform - # (26 × 5 entries of 500-800 MB each) never fits the repository's 10 GB cache limit. + # One entry per platform, shared by every shard: the plugins share most of their + # dependency tree (tauri, wry, tao...), and an entry per package or shard and platform + # never fits the repository's 10 GB cache limit. key: ${{ matrix.platform.target }} # Only `v2`/`v3` write to the cache: an entry saved from a pull request lives on its # merge ref, where nothing else can restore it, and every write evicts an entry # that other runs would have hit (10 GB repository limit, LRU eviction). - # A single package job saves the shared entry, so the jobs don't race for the key. - save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.package == needs.changes.outputs.saver }} + # A single shard saves the shared entry, so the jobs don't race for the key; it is the + # one holding the plugins with the widest dependency trees. + save-if: ${{ (github.ref == 'refs/heads/v2' || github.ref == 'refs/heads/v3') && matrix.shard.save }} # `cross` is built from a pinned git revision (its latest release predates the Android # targets we need). Keep the binary in its own small cache entry (accessed by every run, @@ -292,20 +149,38 @@ jobs: if: ${{ matrix.platform.runner == 'cross' }} run: echo "${{ runner.tool_cache }}/cross/bin" >> $GITHUB_PATH - - name: test ${{ matrix.package }} - if: ${{ matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} - run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets --all-features + # The shard's packages run one after the other, and a failing package does not stop the + # others (like `fail-fast: false` across jobs): the step fails at the end if any did. + - name: ${{ matrix.platform.command }} ${{ matrix.shard.name }} + shell: bash + env: + PACKAGES: ${{ matrix.shard.packages }} + RUNNER: ${{ matrix.platform.runner }} + COMMAND: ${{ matrix.platform.command }} + TARGET: ${{ matrix.platform.target }} + run: | + failed=() + for package in $PACKAGES; do + # the features of `http` and `dialog` cannot all be enabled at once + case "$package" in + tauri-plugin-http|tauri-plugin-dialog) features=() ;; + *) features=(--all-features) ;; + esac - - name: test ${{ matrix.package }} - if: ${{ matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog' }} - run: ${{ matrix.platform.runner }} ${{ matrix.platform.command }} --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-targets + echo "::group::$COMMAND $package" + if ! $RUNNER $COMMAND --package "$package" --target "$TARGET" --all-targets "${features[@]}"; then + failed+=("$package") + # `--all-targets` above does not include doc tests, so they are run separately. + # Only on the platforms we actually test on, since cargo skips doc tests when cross compiling. + elif [ "$COMMAND" = test ] && ! $RUNNER test --doc --package "$package" --target "$TARGET" "${features[@]}"; then + failed+=("$package (doc tests)") + fi + echo "::endgroup::" + done - # `--all-targets` above does not include doc tests, so they are run separately. - # Only on the platforms we actually test on, since cargo skips doc tests when cross compiling. - - name: doc test ${{ matrix.package }} - if: ${{ matrix.platform.command == 'test' && matrix.package != 'tauri-plugin-http' && matrix.package != 'tauri-plugin-dialog' }} - run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} --all-features - - - name: doc test ${{ matrix.package }} - if: ${{ matrix.platform.command == 'test' && (matrix.package == 'tauri-plugin-http' || matrix.package == 'tauri-plugin-dialog') }} - run: ${{ matrix.platform.runner }} test --doc --package ${{ matrix.package }} --target ${{ matrix.platform.target }} + if [ ${#failed[@]} -gt 0 ]; then + for package in "${failed[@]}"; do + echo "::error title=$COMMAND failed::$package" + done + exit 1 + fi diff --git a/.scripts/ci/affected-rust-packages.js b/.scripts/ci/affected-rust-packages.js new file mode 100644 index 000000000..762aa2bb4 --- /dev/null +++ b/.scripts/ci/affected-rust-packages.js @@ -0,0 +1,254 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Lists the plugin crates a change affects, so the Rust workflows only build and test those. +// +// A plugin is affected when: +// - a file in its directory changed (except the JS / docs / Android files `cargo` never reads), +// - a plugin it depends on is affected (e.g. `fs` for `dialog`), or +// - a `Cargo.lock` entry in its dependency tree changed (so a dependency bump only tests the +// plugins that actually use that dependency). +// Every plugin is affected when a workspace-wide file (root `Cargo.toml`, toolchain, cargo +// config, this script, the files in `GLOBAL_PATHS`) changed, or when there is nothing to diff +// against. +// +// Environment: +// - BASE: the revision to diff `HEAD` against; empty or all zeros means every plugin. +// - ALL: `true` to select every plugin. +// - GLOBAL_PATHS: newline separated paths (usually the calling workflow) that affect every plugin. +// - SHARD_SIZE: the maximum number of plugins per shard (default 8). +// +// Writes `packages` (JSON array) and `shards` (JSON array of `{ name, packages, save }`, where +// `packages` is space separated and `save` is true for the shard that should save the cache) +// to `$GITHUB_OUTPUT`. + +import { execFileSync } from 'child_process' +import fs from 'fs' +import path from 'path' + +// Crates whose dependency trees cover most of the other plugins' dependencies. They go first so +// the shard that saves the cache (the first one) restores as much as possible for the others. +const preferredFirst = [ + 'tauri-plugin-upload', + 'tauri-plugin-sql', + 'tauri-plugin-updater', + 'tauri-plugin-single-instance' +] + +const globalPaths = [ + 'Cargo.toml', + 'rust-toolchain', + 'rust-toolchain.toml', + '.cargo/', + '.scripts/ci/affected-rust-packages.js', + ...(process.env.GLOBAL_PATHS ?? '') + .split('\n') + .map((p) => p.trim()) + .filter(Boolean) +] + +// Paths inside a plugin directory that never reach `cargo`. +const ignoredInPlugin = [ + /^guest-js\//, + /^dist-js\//, + /^node_modules\//, + /^android\//, + /(^|\/)[^/]+\.md$/, + /^package\.json$/, + /^tsconfig\.json$/, + /^rollup\.config\.js$/, + /^api-iife\.js$/, + /^banner\.png$/, + /^LICENSE/, + /^contributors\// +] + +function git(...args) { + return execFileSync('git', args, { + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + stdio: ['ignore', 'pipe', 'pipe'] + }) +} + +function readPlugins() { + const plugins = new Map() // directory -> crate name + for (const dir of fs.readdirSync('plugins')) { + const manifest = path.join('plugins', dir, 'Cargo.toml') + if (!fs.existsSync(manifest)) continue + const name = fs + .readFileSync(manifest, 'utf8') + .match(/^\[package\][^[]*?^name\s*=\s*"([^"]+)"/m)?.[1] + if (name) plugins.set(dir, name) + } + return plugins +} + +// Parses the `[[package]]` entries of a Cargo.lock into a map of +// `name version (source)` -> `{ name, version, source, checksum, dependencies, fingerprint }`. +function parseLock(text) { + const packages = new Map() + for (const block of text.split(/^\[\[package\]\]$/m).slice(1)) { + const field = (key) => + block.match(new RegExp(`^${key} = "([^"]*)"`, 'm'))?.[1] + const name = field('name') + const version = field('version') + const source = field('source') + const dependencies = [ + ...(block.match(/^dependencies = \[([^\]]*)\]/m)?.[1] ?? '').matchAll( + /"([^"]+)"/g + ) + ].map((m) => m[1]) + const id = source ? `${name} ${version} (${source})` : `${name} ${version}` + const checksum = field('checksum') + packages.set(id, { name, version, source, checksum, dependencies }) + } + + const byName = new Map() + for (const [id, pkg] of packages) { + byName.set(pkg.name, [...(byName.get(pkg.name) ?? []), id]) + } + // dependency entries are `name`, `name version` or `name version (source)` + const resolve = (dep) => { + if (packages.has(dep)) return dep + const [name, version] = dep.split(' ') + const candidates = byName.get(name) ?? [] + return version + ? candidates.find((id) => packages.get(id).version === version) + : candidates[0] + } + for (const pkg of packages.values()) { + pkg.dependencies = pkg.dependencies.map(resolve).filter(Boolean).sort() + // what identifies the resolved entry: dependencies are compared by what they resolve to, as + // cargo spells a dependency differently (`name` or `name version`) depending on how many + // versions of it the lockfile holds + pkg.fingerprint = JSON.stringify([pkg.checksum, pkg.dependencies]) + } + return packages +} + +function affectedPackages(plugins) { + const all = [...plugins.values()] + const base = process.env.BASE?.trim() + + if (process.env.ALL === 'true') { + console.log('every plugin selected') + return all + } + if (!base || /^0+$/.test(base)) { + console.log('nothing to diff against, selecting every plugin') + return all + } + + let changedFiles + try { + try { + git('cat-file', '-e', `${base}^{commit}`) + } catch { + git('fetch', '--no-tags', '--depth=1', 'origin', base) + } + changedFiles = git('diff', '--name-only', base, 'HEAD') + .split('\n') + .filter(Boolean) + } catch (error) { + console.log( + `could not diff against ${base}, selecting every plugin: ${error.stderr ?? error}` + ) + return all + } + console.log(`changed files:\n ${changedFiles.join('\n ')}`) + + const global = changedFiles.find((file) => + globalPaths.some((p) => (p.endsWith('/') ? file.startsWith(p) : file === p)) + ) + if (global) { + console.log(`${global} affects every plugin`) + return all + } + + const lock = parseLock(fs.readFileSync('Cargo.lock', 'utf8')) + const seeds = new Set() + + for (const file of changedFiles) { + const [, dir, rest] = file.match(/^plugins\/([^/]+)\/(.+)$/) ?? [] + const name = plugins.get(dir) + if (!name || ignoredInPlugin.some((pattern) => pattern.test(rest))) continue + for (const [id, pkg] of lock) { + if (pkg.name === name && !pkg.source) seeds.add(id) + } + } + + if (changedFiles.includes('Cargo.lock')) { + let baseLock + try { + baseLock = parseLock(git('show', `${base}:Cargo.lock`)) + } catch { + console.log('no Cargo.lock to compare with, selecting every plugin') + return all + } + for (const [id, pkg] of lock) { + if (baseLock.get(id)?.fingerprint !== pkg.fingerprint) seeds.add(id) + } + } + + if (seeds.size) console.log(`changed crates:\n ${[...seeds].join('\n ')}`) + + // a plugin is affected when its dependency tree (itself included) reaches a seed, + // so walk the reverse dependency graph from the seeds + const dependents = new Map() + for (const [id, pkg] of lock) { + for (const dep of pkg.dependencies) { + dependents.set(dep, [...(dependents.get(dep) ?? []), id]) + } + } + const reached = new Set(seeds) + const queue = [...seeds] + while (queue.length) { + for (const dependent of dependents.get(queue.pop()) ?? []) { + if (!reached.has(dependent)) { + reached.add(dependent) + queue.push(dependent) + } + } + } + + const reachedWorkspaceCrates = new Set( + [...reached] + .map((id) => lock.get(id)) + .filter((pkg) => !pkg.source) + .map((pkg) => pkg.name) + ) + return all.filter((name) => reachedWorkspaceCrates.has(name)) +} + +const plugins = readPlugins() +const packages = affectedPackages(plugins).sort((a, b) => { + const rank = (p) => + preferredFirst.includes(p) ? preferredFirst.indexOf(p) : Infinity + return rank(a) - rank(b) || a.localeCompare(b) +}) + +const shardSize = Number(process.env.SHARD_SIZE) || 8 +const shardCount = Math.ceil(packages.length / shardSize) +const perShard = Math.ceil(packages.length / Math.max(shardCount, 1)) +const shards = Array.from({ length: shardCount }, (_, i) => { + const shard = packages.slice(i * perShard, (i + 1) * perShard) + return { + name: + shard.length <= 3 + ? shard.map((p) => p.replace('tauri-plugin-', '')).join(', ') + : `${i + 1}/${shardCount}`, + packages: shard.join(' '), + save: i === 0 + } +}) + +console.log(`affected packages:\n ${packages.join('\n ') || '(none)'}`) + +if (process.env.GITHUB_OUTPUT) { + fs.appendFileSync( + process.env.GITHUB_OUTPUT, + `packages=${JSON.stringify(packages)}\nshards=${JSON.stringify(shards)}\n` + ) +} diff --git a/plugins/autostart/src/lib.rs b/plugins/autostart/src/lib.rs index 62f27db87..fa799a09b 100644 --- a/plugins/autostart/src/lib.rs +++ b/plugins/autostart/src/lib.rs @@ -37,7 +37,7 @@ pub enum MacosLauncher { #[cfg(target_os = "macos")] impl MacosLauncher { - fn to_auto_launch(&self) -> auto_launch::MacOSLaunchMode { + fn to_auto_launch(self) -> auto_launch::MacOSLaunchMode { match self { MacosLauncher::LaunchAgent => auto_launch::MacOSLaunchMode::LaunchAgent, MacosLauncher::AppleScript => auto_launch::MacOSLaunchMode::AppleScript, diff --git a/plugins/biometric/Cargo.toml b/plugins/biometric/Cargo.toml index 26ba86322..f5803e387 100644 --- a/plugins/biometric/Cargo.toml +++ b/plugins/biometric/Cargo.toml @@ -31,3 +31,6 @@ tauri = { workspace = true } log = { workspace = true } thiserror = { workspace = true } serde_repr = "0.1" + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] } diff --git a/plugins/nfc/Cargo.toml b/plugins/nfc/Cargo.toml index 9f6347f80..bfe48b22f 100644 --- a/plugins/nfc/Cargo.toml +++ b/plugins/nfc/Cargo.toml @@ -30,3 +30,6 @@ tauri = { workspace = true } log = { workspace = true } thiserror = { workspace = true } serde_repr = "0.1" + +[target.'cfg(target_os = "ios")'.dependencies] +tauri = { workspace = true, features = ["wry"] }