mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-10-29 11:34:38 +08:00
Merge remote-tracking branch 'origin/dev' into v3
This commit is contained in:
59 files changed
+3371
-2283
No files matched your search
@@ -9,6 +9,12 @@ ignore = [
|
||||
# rand unsoundness, fixed when we remove kuchikiki from deps in v3, currently
|
||||
# remains for semver reasons but is not built in default configuration
|
||||
"RUSTSEC-2026-0097",
|
||||
# This ruzstd vulnerability is denial of service only used in CLI for devs,
|
||||
# the fix requires an upstream update in https://github.com/indygreg/apple-platform-rs/pull/342
|
||||
"RUSTSEC-2026-0344",
|
||||
# glib 0.18.5 unsoundness, fixed by updating to gtk4
|
||||
"RUSTSEC-2024-0429",
|
||||
# ml-dsa timing side-channel, only reachable when signing RPMs with a post-quantum ML-DSA key locally,
|
||||
# pulled in by rpm (enables pgp's draft-pqc feature); no pgp release uses a patched ml-dsa yet
|
||||
"RUSTSEC-2025-0144",
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tauri-apps/api": minor:changes
|
||||
"tauri-macos-sign": minor:changes
|
||||
---
|
||||
|
||||
Ported changes from v2.
|
||||
@@ -46,6 +46,16 @@ jobs:
|
||||
ext: ''
|
||||
args: ''
|
||||
cross: true
|
||||
- os: ubuntu-22.04
|
||||
rust_target: aarch64-unknown-linux-gnu
|
||||
ext: ''
|
||||
args: ''
|
||||
cross: true
|
||||
- os: ubuntu-22.04
|
||||
rust_target: aarch64-unknown-linux-musl
|
||||
ext: ''
|
||||
args: ''
|
||||
cross: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
name: test `tauri ios xcode-script` project resolution
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# Package manager releases change the directory and environment Xcode's build script runs
|
||||
# with (pnpm 12.3 broke it without any change on our side), so the latest releases are
|
||||
# checked every week too.
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
paths:
|
||||
- '.github/workflows/test-cli-xcode-script.yml'
|
||||
- '.scripts/ci/xcode-script-resolution.mjs'
|
||||
- 'crates/tauri-cli/src/mobile/ios/**'
|
||||
- 'crates/tauri-cli/src/mobile/init.rs'
|
||||
- 'crates/tauri-cli/src/helpers/app_paths.rs'
|
||||
- 'crates/tauri-utils/src/config/parse.rs'
|
||||
- 'packages/cli/*.js'
|
||||
- 'packages/cli/src/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/test-cli-xcode-script.yml'
|
||||
- '.scripts/ci/xcode-script-resolution.mjs'
|
||||
- 'crates/tauri-cli/src/mobile/ios/**'
|
||||
- 'crates/tauri-cli/src/mobile/init.rs'
|
||||
- 'crates/tauri-cli/src/helpers/app_paths.rs'
|
||||
- 'crates/tauri-utils/src/config/parse.rs'
|
||||
- 'packages/cli/*.js'
|
||||
- 'packages/cli/src/**'
|
||||
|
||||
env:
|
||||
RUST_BACKTRACE: 1
|
||||
CARGO_PROFILE_DEV_DEBUG: 0 # This would add unnecessary bloat to the target folder, decreasing cache efficiency.
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
# the `ios` commands only exist in the macOS CLI; Xcode itself is not used
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: install Rust 1.90
|
||||
# the MSRV, so this job restores the `tauri-cli` cache entry saved by test-cli-rs
|
||||
uses: dtolnay/rust-toolchain@1.90
|
||||
|
||||
- run: npm i -g --force corepack
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
cache: 'pnpm'
|
||||
|
||||
# the other package managers are installed from npm by the script
|
||||
- uses: denoland/setup-deno@v2
|
||||
with:
|
||||
deno-version: v2.x
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
# Restore-only: test-cli-rs saves this entry on `dev`; see test-cli-rs.yml.
|
||||
shared-key: tauri-cli
|
||||
save-if: false
|
||||
|
||||
- name: build the CLI
|
||||
# Debug profile: only the project lookup is exercised, and a debug build reuses the
|
||||
# cached dependencies instead of a from-scratch LTO build.
|
||||
run: |
|
||||
pnpm i --frozen-lockfile --filter @tauri-apps/cli
|
||||
pnpm build:cli:debug
|
||||
cargo build -p tauri-cli --bin cargo-tauri
|
||||
|
||||
- name: test
|
||||
timeout-minutes: 20
|
||||
run: node .scripts/ci/xcode-script-resolution.mjs ${{ github.event_name == 'schedule' && '--latest' || '' }}
|
||||
@@ -15,6 +15,12 @@ const bundlerLicense =
|
||||
'// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>'
|
||||
const denoLicense =
|
||||
'// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.'
|
||||
// files vendored from apple-codesign keep their original MPL-2.0 notice
|
||||
const mplLicense =
|
||||
'// This Source Code Form is subject to the terms of the Mozilla Public'
|
||||
const mplVendored = [
|
||||
path.join('crates', 'tauri-macos-sign', 'src', 'apple_codesign')
|
||||
]
|
||||
|
||||
const extensions = ['.rs', '.js', '.ts', '.yml', '.swift', '.kt']
|
||||
const ignore = [
|
||||
@@ -40,6 +46,11 @@ async function checkFile(file) {
|
||||
let contents = ``
|
||||
let i = 0
|
||||
for await (let line of rl) {
|
||||
// accept vendored apple-codesign code under the MPL-2.0 license
|
||||
if (line === mplLicense && mplVendored.some((d) => file.includes(d))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// ignore empty lines, allow shebang and bundler license
|
||||
if (
|
||||
line.length === 0
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Checks that `tauri ios xcode-script` finds the Tauri project for every package manager, project layout
|
||||
// and way Xcode can be started.
|
||||
//
|
||||
// Xcode runs the command the iOS project template generates (`pnpm tauri ios xcode-script ...`, see
|
||||
// crates/tauri-cli/src/mobile/init.rs) from `<tauri dir>/gen/apple`. Whether the package manager then moves
|
||||
// to the package root, and which environment variables it sets, depends on the package manager and its
|
||||
// version, and Xcode inherits the environment of the `tauri ios dev` process that opened it (`--open`) or
|
||||
// spawned xcodebuild. This script reproduces each combination without Xcode: no dev server is running, so
|
||||
// the CLI fails reading `<resolved tauri dir>/gen/apple/.tauri/cli-options-server.json`, and that path tells
|
||||
// which directory it resolved.
|
||||
//
|
||||
// The CLI runs in-process through each package manager as with a real install: the fixtures depend on a
|
||||
// copy of the `@tauri-apps/cli` JS wrapper whose native module is loaded from `--cli-node`.
|
||||
//
|
||||
// Usage: node .scripts/ci/xcode-script-resolution.mjs [--cli-node <cli.node>] [--cli-bin <cargo-tauri>]
|
||||
// [--pm <regex>] [--latest] [--keep]
|
||||
// Defaults to the `pnpm build:cli:debug` and `cargo build -p tauri-cli` outputs. `--latest` tests the
|
||||
// latest release of each package manager instead of the pinned versions.
|
||||
|
||||
import { spawn, execFileSync } from 'node:child_process'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { parseArgs } from 'node:util'
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..', '..')
|
||||
|
||||
const { values: opts } = parseArgs({
|
||||
options: {
|
||||
'cli-node': { type: 'string' },
|
||||
'cli-bin': {
|
||||
type: 'string',
|
||||
default: path.join(ROOT, 'target', 'debug', 'cargo-tauri')
|
||||
},
|
||||
pm: { type: 'string' },
|
||||
latest: { type: 'boolean', default: false },
|
||||
keep: { type: 'boolean', default: false }
|
||||
}
|
||||
})
|
||||
|
||||
const CLI_NODE = path.resolve(
|
||||
opts['cli-node']
|
||||
?? path.join(
|
||||
ROOT,
|
||||
'packages',
|
||||
'cli',
|
||||
`cli.${process.platform}-${process.arch}.node`
|
||||
)
|
||||
)
|
||||
const CLI_BIN = path.resolve(opts['cli-bin'])
|
||||
for (const f of [CLI_NODE, CLI_BIN]) {
|
||||
if (!fs.existsSync(f)) {
|
||||
console.error(
|
||||
`${f} not found, build the CLI first (pnpm build:cli:debug && cargo build -p tauri-cli)`
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const WORK = fs.realpathSync(
|
||||
fs.mkdtempSync(path.join(os.tmpdir(), 'tauri-xcode-script-'))
|
||||
)
|
||||
const TOOLS = path.join(WORK, 'tools')
|
||||
const FIXTURES = path.join(WORK, 'fixtures')
|
||||
const FAKE_CLI = path.join(WORK, 'cli')
|
||||
const BIN = path.join(WORK, 'bin')
|
||||
|
||||
// `version` is installed from npm (`pkg@version`). `--latest` tests the latest release instead, keeping one
|
||||
// entry per package manager kind and linker.
|
||||
const PINNED_PMS = [
|
||||
{ id: 'npm-10', kind: 'npm', pkg: 'npm', version: '10' },
|
||||
{ id: 'npm-11', kind: 'npm', pkg: 'npm', version: '11' },
|
||||
{ id: 'npx-11', kind: 'npx', pkg: 'npm', version: '11' },
|
||||
{ id: 'pnpm-9', kind: 'pnpm', pkg: 'pnpm', version: '9' },
|
||||
{ id: 'pnpm-10', kind: 'pnpm', pkg: 'pnpm', version: '10' },
|
||||
// runs the bin in place, without lifecycle env vars, when called outside a package
|
||||
{ id: 'pnpm-12.3', kind: 'pnpm', pkg: 'pnpm', version: '12.3' },
|
||||
{ id: 'pnpm-12', kind: 'pnpm', pkg: 'pnpm', version: '12' },
|
||||
{ id: 'yarn-1', kind: 'yarn1', pkg: 'yarn', version: '1' },
|
||||
{
|
||||
id: 'yarn-4-node-modules',
|
||||
kind: 'yarn4',
|
||||
linker: 'node-modules',
|
||||
pkg: '@yarnpkg/cli-dist',
|
||||
version: '4'
|
||||
},
|
||||
{
|
||||
id: 'yarn-4-pnp',
|
||||
kind: 'yarn4',
|
||||
linker: 'pnp',
|
||||
pkg: '@yarnpkg/cli-dist',
|
||||
version: '4'
|
||||
},
|
||||
// bun<1.2 does not move to the package root
|
||||
{ id: 'bun-1.1', kind: 'bun', pkg: 'bun', version: '1.1' },
|
||||
{ id: 'bun-1', kind: 'bun', pkg: 'bun', version: '1' },
|
||||
// uses the `deno` on PATH; dependencies are installed with npm
|
||||
{ id: 'deno', kind: 'deno', pkg: 'npm', version: '11' },
|
||||
{ id: 'cargo', kind: 'cargo' }
|
||||
]
|
||||
|
||||
const PMS = opts.latest
|
||||
? [
|
||||
...new Map(
|
||||
PINNED_PMS.map((pm) => [
|
||||
`${pm.kind}-${pm.linker}`,
|
||||
pm.version
|
||||
? {
|
||||
...pm,
|
||||
id: pm.id.replace(`-${pm.version}`, '-latest'),
|
||||
version: 'latest'
|
||||
}
|
||||
: pm
|
||||
])
|
||||
).values()
|
||||
]
|
||||
: PINNED_PMS
|
||||
|
||||
const npmInstall = ['npm', 'install', '--no-audit', '--no-fund']
|
||||
|
||||
// how each kind of package manager is installed and runs the `tauri` script; `run` and `filter` match the
|
||||
// `tauri-binary` + `tauri-binary-args` the iOS project template is generated with (init.rs), `filter` runs
|
||||
// it for a workspace member from the workspace root (e.g. `pnpm --filter app tauri ios dev`)
|
||||
const KINDS = {
|
||||
npm: {
|
||||
bin: 'npm',
|
||||
install: npmInstall,
|
||||
run: (args) => ['npm', 'run', '--', 'tauri', ...args],
|
||||
filter: (args, s) => ['npm', 'run', 'tauri', '-w', s.pkg, '--', ...args]
|
||||
},
|
||||
npx: {
|
||||
bin: 'npm',
|
||||
install: npmInstall,
|
||||
run: (args) => ['npx', 'tauri', ...args]
|
||||
},
|
||||
pnpm: {
|
||||
bin: 'pnpm',
|
||||
install: ['pnpm', 'install'],
|
||||
packageManager: 'pnpm',
|
||||
run: (args) => ['pnpm', 'tauri', ...args],
|
||||
filter: (args) => ['pnpm', '--filter', 'app', 'tauri', ...args]
|
||||
},
|
||||
yarn1: {
|
||||
bin: 'yarn',
|
||||
install: ['yarn', 'install', '--no-progress', '--non-interactive'],
|
||||
run: (args) => ['yarn', 'tauri', ...args],
|
||||
filter: (args) => ['yarn', 'workspace', 'app', 'tauri', ...args]
|
||||
},
|
||||
yarn4: {
|
||||
bin: 'yarn',
|
||||
install: ['yarn', 'install'],
|
||||
packageManager: 'yarn',
|
||||
run: (args) => ['yarn', 'tauri', ...args],
|
||||
filter: (args) => ['yarn', 'workspace', 'app', 'tauri', ...args]
|
||||
},
|
||||
bun: {
|
||||
bin: 'bun',
|
||||
install: ['bun', 'install'],
|
||||
run: (args) => ['bun', 'tauri', ...args],
|
||||
filter: (args) => ['bun', '--filter', 'app', 'tauri', ...args]
|
||||
},
|
||||
deno: {
|
||||
bin: 'deno',
|
||||
install: npmInstall,
|
||||
run: (args) => ['deno', 'task', 'tauri', ...args]
|
||||
},
|
||||
// cargo leaves no package manager environment behind, so only `clean` applies
|
||||
cargo: { run: (args) => ['cargo', 'tauri', ...args], cleanOnly: true }
|
||||
}
|
||||
|
||||
// `pkg` = dir with the package.json owning the `tauri` script, `tauri` = Tauri dir, `ws` = workspace root.
|
||||
// `decoys` are other Tauri apps sorting before the tested one, to catch a lookup from the workspace root.
|
||||
const SCENARIOS = [
|
||||
{ id: 'standalone', pkg: '.', tauri: 'src-tauri' },
|
||||
{ id: 'custom-tauri-dir', pkg: '.', tauri: 'desktop' },
|
||||
{ id: 'tauri-dir-is-pkg-root', pkg: '.', tauri: '.' },
|
||||
{
|
||||
id: 'workspace-member',
|
||||
ws: '.',
|
||||
pkg: 'apps/app',
|
||||
tauri: 'apps/app/src-tauri',
|
||||
decoys: ['apps/aaa-other']
|
||||
},
|
||||
// like tauri-apps/plugins-workspace: the workspace root also depends on the CLI
|
||||
{
|
||||
id: 'workspace-member-root-cli',
|
||||
ws: '.',
|
||||
pkg: 'apps/app',
|
||||
tauri: 'apps/app/src-tauri',
|
||||
rootCli: true,
|
||||
decoys: ['apps/aaa-other']
|
||||
},
|
||||
{
|
||||
id: 'workspace-member-nested',
|
||||
ws: '.',
|
||||
pkg: 'packages/desktop/app',
|
||||
tauri: 'packages/desktop/app/src-tauri',
|
||||
decoys: ['packages/desktop/aaa-other']
|
||||
},
|
||||
// the CLI and `tauri` script live in the workspace root, the app member only has a frontend package.json
|
||||
{
|
||||
id: 'workspace-root-owns-cli',
|
||||
ws: '.',
|
||||
pkg: '.',
|
||||
member: 'apps/app',
|
||||
tauri: 'apps/app/src-tauri'
|
||||
},
|
||||
{ id: 'no-package-json', pkg: null, tauri: 'src-tauri', cargoOnly: true }
|
||||
]
|
||||
|
||||
// where `tauri ios dev` was started, which decides the environment Xcode inherits
|
||||
const MODES = [
|
||||
{ id: 'clean' }, // Xcode opened on its own
|
||||
{ id: 'dev@pkg', start: (s) => s.pkg },
|
||||
{ id: 'dev@tauri', start: (s) => s.tauri },
|
||||
{ id: 'dev@ws-filter', start: (s) => s.ws, filter: true } // e.g. `pnpm --filter app tauri ios dev`
|
||||
]
|
||||
|
||||
// Combinations where the package manager itself cannot start the CLI, either for `tauri ios dev` (`dev-failed`)
|
||||
// or for `xcode-script` (`cli-not-reached`). These are allowed to fail before the CLI runs (not required to,
|
||||
// so newer package manager releases may fix them); a CLI that runs but does not find the right Tauri dir is
|
||||
// always a failure.
|
||||
const PM_LIMITATIONS = [
|
||||
{
|
||||
pm: /^pnpm-12\.3$/,
|
||||
modes: ['clean', 'dev@tauri'],
|
||||
why: 'pnpm@12.3 does not look up bins outside a package'
|
||||
},
|
||||
{
|
||||
kind: /^(npm|yarn4)$/,
|
||||
scenario: 'workspace-root-owns-cli',
|
||||
why: 'the member package.json has no `tauri` script'
|
||||
}
|
||||
]
|
||||
|
||||
const XCODE_ARGS = [
|
||||
'ios',
|
||||
'xcode-script',
|
||||
'--platform',
|
||||
'iOS Simulator',
|
||||
'--sdk-root',
|
||||
'/sdk',
|
||||
'--framework-search-paths',
|
||||
'x',
|
||||
'--header-search-paths',
|
||||
'x',
|
||||
'--gcc-preprocessor-definitions',
|
||||
'DEBUG=1',
|
||||
'--configuration',
|
||||
'debug',
|
||||
'arm64'
|
||||
]
|
||||
|
||||
const which = (bin) => {
|
||||
try {
|
||||
return execFileSync('/usr/bin/which', [bin]).toString().trim()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const SYSTEM_PATH = ['node', 'deno', 'cargo']
|
||||
.map(which)
|
||||
.filter(Boolean)
|
||||
.map((p) => path.dirname(p))
|
||||
|
||||
// runs in a process group of its own so a timeout also kills the CLI the package manager started
|
||||
function run(cmd, args, { cwd, env, timeout = 120000 }) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(cmd, args, {
|
||||
cwd,
|
||||
env,
|
||||
detached: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
})
|
||||
let out = ''
|
||||
child.stdout.on('data', (d) => (out += d))
|
||||
child.stderr.on('data', (d) => (out += d))
|
||||
const timer = setTimeout(() => {
|
||||
out += `\n(timed out after ${timeout / 1000}s)`
|
||||
try {
|
||||
process.kill(-child.pid, 'SIGKILL')
|
||||
} catch {}
|
||||
}, timeout)
|
||||
child.on('error', (e) => {
|
||||
clearTimeout(timer)
|
||||
resolve({ code: -1, out: out + String(e) })
|
||||
})
|
||||
child.on('close', (code) => {
|
||||
clearTimeout(timer)
|
||||
resolve({ code, out })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const write = (p, content) => {
|
||||
fs.mkdirSync(path.dirname(p), { recursive: true })
|
||||
fs.writeFileSync(
|
||||
p,
|
||||
typeof content === 'string' ? content : JSON.stringify(content, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
function setupCli() {
|
||||
for (const f of ['tauri.js', 'main.js']) {
|
||||
fs.cpSync(path.join(ROOT, 'packages', 'cli', f), path.join(FAKE_CLI, f))
|
||||
}
|
||||
write(
|
||||
path.join(FAKE_CLI, 'index.js'),
|
||||
[
|
||||
'// dumps the environment `tauri ios dev` would run with, or loads the native CLI under test',
|
||||
'if (process.env.TAURI_XCODE_SCRIPT_TEST_DUMP) {',
|
||||
" require('fs').writeFileSync(process.env.TAURI_XCODE_SCRIPT_TEST_DUMP, JSON.stringify(process.env))",
|
||||
' process.exit(0)',
|
||||
'}',
|
||||
`module.exports = require(${JSON.stringify(CLI_NODE)})`,
|
||||
''
|
||||
].join('\n')
|
||||
)
|
||||
write(path.join(FAKE_CLI, 'package.json'), {
|
||||
name: '@tauri-apps/cli',
|
||||
version: '0.0.0',
|
||||
type: 'commonjs',
|
||||
main: 'main.js',
|
||||
bin: { tauri: 'tauri.js' }
|
||||
})
|
||||
fs.mkdirSync(BIN, { recursive: true })
|
||||
fs.symlinkSync(CLI_BIN, path.join(BIN, 'cargo-tauri'))
|
||||
}
|
||||
|
||||
// installs the package manager from npm and exposes its binaries in a PATH dir of its own
|
||||
async function setupPm(pm) {
|
||||
const kind = KINDS[pm.kind]
|
||||
pm.path = path.join(TOOLS, pm.id, 'path')
|
||||
fs.mkdirSync(pm.path, { recursive: true })
|
||||
if (pm.kind === 'deno' && !which('deno')) return 'deno is not installed'
|
||||
if (!pm.pkg) return null
|
||||
const dir = path.join(TOOLS, pm.id)
|
||||
const spec = `${pm.pkg}@${pm.version}`
|
||||
const res = await run(
|
||||
'npm',
|
||||
['install', '--prefix', dir, '--no-audit', '--no-fund', '--no-save', spec],
|
||||
{ cwd: WORK, env: process.env, timeout: 300000 }
|
||||
)
|
||||
if (res.code !== 0) return `failed to install ${spec}:\n${res.out}`
|
||||
const bins = path.join(dir, 'node_modules', '.bin')
|
||||
for (const bin of fs.readdirSync(bins))
|
||||
fs.symlinkSync(path.join(bins, bin), path.join(pm.path, bin))
|
||||
const version = await run(kind.bin, ['--version'], {
|
||||
cwd: WORK,
|
||||
env: env(pm)
|
||||
})
|
||||
pm.resolved = version.out.match(/\d+\.\d+\.\d+\S*/)?.[0]
|
||||
return null
|
||||
}
|
||||
|
||||
function env(pm, extra = {}) {
|
||||
const dirs = [
|
||||
pm.path,
|
||||
BIN,
|
||||
...SYSTEM_PATH,
|
||||
'/usr/bin',
|
||||
'/bin',
|
||||
'/usr/sbin',
|
||||
'/sbin'
|
||||
]
|
||||
return {
|
||||
HOME: process.env.HOME,
|
||||
USER: process.env.USER,
|
||||
TMPDIR: process.env.TMPDIR,
|
||||
LANG: 'en_US.UTF-8',
|
||||
PATH: [...new Set(dirs)].join(':'),
|
||||
RUSTUP_HOME:
|
||||
process.env.RUSTUP_HOME ?? path.join(process.env.HOME, '.rustup'),
|
||||
CARGO_HOME: process.env.CARGO_HOME ?? path.join(process.env.HOME, '.cargo'),
|
||||
RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN,
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
YARN_ENABLE_IMMUTABLE_INSTALLS: 'false',
|
||||
YARN_ENABLE_TELEMETRY: '0',
|
||||
YARN_ENABLE_GLOBAL_CACHE: 'false',
|
||||
NO_COLOR: '1',
|
||||
...extra
|
||||
}
|
||||
}
|
||||
|
||||
function writeTauriDir(dir, name) {
|
||||
write(path.join(dir, 'tauri.conf.json'), {
|
||||
productName: name,
|
||||
version: '0.1.0',
|
||||
identifier: `com.tauri.${name}`,
|
||||
build: { frontendDist: [] },
|
||||
app: { windows: [] }
|
||||
})
|
||||
write(path.join(dir, 'gen', 'apple', 'project.yml'), `name: ${name}\n`)
|
||||
}
|
||||
|
||||
function packageJson(pm, name, { cli = true, workspaces } = {}) {
|
||||
const pkg = { name, version: '0.0.0', private: true }
|
||||
if (cli) {
|
||||
// deno users run the npm package through deno itself
|
||||
pkg.scripts = {
|
||||
tauri: pm.kind === 'deno' ? 'deno run -A npm:@tauri-apps/cli' : 'tauri'
|
||||
}
|
||||
pkg.devDependencies = { '@tauri-apps/cli': `file:${FAKE_CLI}` }
|
||||
}
|
||||
if (workspaces && pm.kind !== 'pnpm') pkg.workspaces = workspaces
|
||||
// pnpm@>=10 and yarn@>=2 switch to the version in `packageManager`, so pin the one under test
|
||||
const manager = KINDS[pm.kind].packageManager
|
||||
if (manager) pkg.packageManager = `${manager}@${pm.resolved}`
|
||||
return pkg
|
||||
}
|
||||
|
||||
function createFixture(pm, s) {
|
||||
const root = path.join(FIXTURES, pm.id, s.id)
|
||||
const at = (p) => path.join(root, p)
|
||||
fs.mkdirSync(root, { recursive: true })
|
||||
|
||||
if (s.ws) {
|
||||
const members = [s.pkg, s.member, ...(s.decoys ?? [])].filter(
|
||||
(p) => p && p !== '.'
|
||||
)
|
||||
const globs = [...new Set(members.map((p) => `${path.dirname(p)}/*`))]
|
||||
const wsPkg = packageJson(pm, 'workspace', {
|
||||
cli: s.pkg === '.',
|
||||
workspaces: globs
|
||||
})
|
||||
if (s.rootCli)
|
||||
wsPkg.devDependencies = { '@tauri-apps/cli': `file:${FAKE_CLI}` }
|
||||
write(at('package.json'), wsPkg)
|
||||
if (pm.kind === 'pnpm') {
|
||||
write(
|
||||
at('pnpm-workspace.yaml'),
|
||||
`packages:\n${globs.map((g) => ` - '${g}'`).join('\n')}\n`
|
||||
)
|
||||
}
|
||||
if (s.pkg !== '.')
|
||||
write(at(path.join(s.pkg, 'package.json')), packageJson(pm, 'app'))
|
||||
if (s.member)
|
||||
write(
|
||||
at(path.join(s.member, 'package.json')),
|
||||
packageJson(pm, 'app', { cli: false })
|
||||
)
|
||||
for (const decoy of s.decoys ?? []) {
|
||||
write(
|
||||
at(path.join(decoy, 'package.json')),
|
||||
packageJson(pm, path.basename(decoy))
|
||||
)
|
||||
writeTauriDir(at(path.join(decoy, 'src-tauri')), 'decoy')
|
||||
}
|
||||
} else if (s.pkg) {
|
||||
write(at(path.join(s.pkg, 'package.json')), packageJson(pm, 'app'))
|
||||
}
|
||||
writeTauriDir(at(s.tauri), 'app')
|
||||
if (pm.kind === 'yarn4')
|
||||
write(
|
||||
at('.yarnrc.yml'),
|
||||
`nodeLinker: ${pm.linker}\nenableGlobalCache: false\n`
|
||||
)
|
||||
if (pm.kind === 'yarn1') write(at('yarn.lock'), '')
|
||||
return root
|
||||
}
|
||||
|
||||
async function install(pm, root) {
|
||||
const cmd = KINDS[pm.kind].install
|
||||
if (!cmd) return null
|
||||
const res = await run(cmd[0], cmd.slice(1), {
|
||||
cwd: root,
|
||||
env: env(pm),
|
||||
timeout: 300000
|
||||
})
|
||||
return res.code === 0 ? null : `\`${cmd.join(' ')}\` failed:\n${res.out}`
|
||||
}
|
||||
|
||||
// runs `tauri ios dev` with the dump hook to get the environment Xcode would inherit
|
||||
async function captureDevEnv(pm, s, root, mode, inv) {
|
||||
const dump = path.join(root, `.env-${mode.id}.json`)
|
||||
const res = await run(inv[0], inv.slice(1), {
|
||||
cwd: path.join(root, mode.start(s)),
|
||||
env: env(pm, { TAURI_XCODE_SCRIPT_TEST_DUMP: dump })
|
||||
})
|
||||
if (!fs.existsSync(dump)) {
|
||||
return {
|
||||
status: 'dev-failed',
|
||||
detail: `\`${inv.join(' ')}\` did not run from ${mode.start(s)}:\n${tail(res.out)}`
|
||||
}
|
||||
}
|
||||
const captured = JSON.parse(fs.readFileSync(dump, 'utf8'))
|
||||
delete captured.TAURI_XCODE_SCRIPT_TEST_DUMP
|
||||
return { env: captured }
|
||||
}
|
||||
|
||||
const tail = (out) => out.trim().split('\n').slice(-8).join('\n')
|
||||
|
||||
async function runXcodeScript(pm, s, root, devEnv) {
|
||||
const inv = KINDS[pm.kind].run(XCODE_ARGS)
|
||||
const res = await run(inv[0], inv.slice(1), {
|
||||
cwd: path.join(root, s.tauri, 'gen', 'apple'),
|
||||
env: devEnv ? { ...devEnv, PATH: `${BIN}:${devEnv.PATH}` } : env(pm)
|
||||
})
|
||||
const resolved = res.out.match(
|
||||
/failed to read (\S+?)\/gen\/apple\/\.tauri\/cli-options-server\.json/
|
||||
)
|
||||
if (resolved) {
|
||||
const expected = fs.realpathSync(path.join(root, s.tauri))
|
||||
const got = fs.realpathSync(resolved[1])
|
||||
return got === expected
|
||||
? { status: 'pass' }
|
||||
: {
|
||||
status: 'wrong-dir',
|
||||
detail: `resolved ${path.relative(root, got) || '.'}`
|
||||
}
|
||||
}
|
||||
if (/Couldn't recognize the .* folder as a Tauri project/.test(res.out)) {
|
||||
return { status: 'not-found', detail: tail(res.out) }
|
||||
}
|
||||
return { status: 'cli-not-reached', detail: tail(res.out) }
|
||||
}
|
||||
|
||||
async function runCase(pm, s, root, mode) {
|
||||
if (!mode.start) return runXcodeScript(pm, s, root, null)
|
||||
const kind = KINDS[pm.kind]
|
||||
const args = ['ios', 'dev', '--open']
|
||||
const inv = mode.filter ? kind.filter?.(args, s) : kind.run(args)
|
||||
if (!inv) return { status: 'n/a', detail: 'no such command' }
|
||||
const captured = await captureDevEnv(pm, s, root, mode, inv)
|
||||
return captured.env ? runXcodeScript(pm, s, root, captured.env) : captured
|
||||
}
|
||||
|
||||
const limitation = (r) =>
|
||||
PM_LIMITATIONS.find(
|
||||
(l) =>
|
||||
(!l.pm || l.pm.test(r.pm.id))
|
||||
&& (!l.kind || l.kind.test(r.pm.kind))
|
||||
&& (!l.scenario || l.scenario === r.s.id)
|
||||
&& (!l.modes || l.modes.includes(r.mode.id))
|
||||
)
|
||||
|
||||
// `pass`, `n/a` (the package manager has no such command), `limitation` (see PM_LIMITATIONS) or `fail`
|
||||
function verdict(r) {
|
||||
if (r.status === 'pass' || r.status === 'n/a') return r.status
|
||||
if (['dev-failed', 'cli-not-reached'].includes(r.status) && r.limitation)
|
||||
return 'limitation'
|
||||
return 'fail'
|
||||
}
|
||||
|
||||
async function pool(items, size, fn) {
|
||||
let next = 0
|
||||
await Promise.all(
|
||||
Array.from({ length: size }, async () => {
|
||||
while (next < items.length) await fn(items[next++])
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
setupCli()
|
||||
const pmFilter = opts.pm ? new RegExp(opts.pm) : null
|
||||
const pms = PMS.filter((pm) => !pmFilter || pmFilter.test(pm.id))
|
||||
const rows = []
|
||||
const setupErrors = []
|
||||
|
||||
await pool(pms, 4, async (pm) => {
|
||||
const error = await setupPm(pm)
|
||||
if (error) {
|
||||
setupErrors.push(`${pm.id}: ${error}`)
|
||||
return
|
||||
}
|
||||
for (const s of SCENARIOS) {
|
||||
if (s.cargoOnly && pm.kind !== 'cargo') continue
|
||||
const root = createFixture(pm, s)
|
||||
const installError = await install(pm, root)
|
||||
if (installError) {
|
||||
setupErrors.push(`${pm.id} ${s.id}: ${installError}`)
|
||||
continue
|
||||
}
|
||||
for (const mode of MODES) {
|
||||
// filtering only applies when the `tauri` script lives in a workspace member
|
||||
if (mode.filter && (!s.ws || s.pkg === '.')) continue
|
||||
if (mode.start && KINDS[pm.kind].cleanOnly) continue
|
||||
if (mode.id === 'dev@tauri' && s.tauri === s.pkg) continue
|
||||
const r = { pm, s, mode, ...(await runCase(pm, s, root, mode)) }
|
||||
r.limitation = limitation(r)
|
||||
r.verdict = verdict(r)
|
||||
rows.push(r)
|
||||
}
|
||||
}
|
||||
console.log(`${pm.id} (${pm.resolved ?? pm.kind}) done`)
|
||||
})
|
||||
|
||||
const order = (list, item) => list.indexOf(item)
|
||||
rows.sort(
|
||||
(a, b) =>
|
||||
order(PMS, a.pm) - order(PMS, b.pm)
|
||||
|| order(SCENARIOS, a.s) - order(SCENARIOS, b.s)
|
||||
|| order(MODES, a.mode) - order(MODES, b.mode)
|
||||
)
|
||||
const failures = rows.filter((r) => r.verdict === 'fail')
|
||||
|
||||
const counts = {}
|
||||
for (const r of rows) counts[r.verdict] = (counts[r.verdict] ?? 0) + 1
|
||||
console.log(`\n${rows.length} cases: ${JSON.stringify(counts)}`)
|
||||
for (const r of rows.filter((r) => r.verdict === 'limitation')) {
|
||||
console.log(
|
||||
`known limitation: ${r.pm.id} ${r.s.id} ${r.mode.id}: ${r.limitation.why}`
|
||||
)
|
||||
}
|
||||
for (const r of failures) {
|
||||
console.log(
|
||||
`\nFAIL ${r.pm.id} (${r.pm.resolved ?? r.pm.kind}) ${r.s.id} ${r.mode.id}: ${r.status}`
|
||||
)
|
||||
if (r.detail) console.log(` ${r.detail.replace(/\n/g, '\n ')}`)
|
||||
}
|
||||
for (const e of setupErrors) console.log(`\nSETUP ERROR ${e}`)
|
||||
|
||||
if (process.env.GITHUB_STEP_SUMMARY) {
|
||||
const ICONS = { pass: '✅', fail: '❌', limitation: '⚪', 'n/a': '➖' }
|
||||
const cols = MODES.map((m) => m.id)
|
||||
let md = `### \`tauri ios xcode-script\` project resolution\n\n${rows.length} cases, ${failures.length} failures`
|
||||
md +=
|
||||
' (✅ pass, ❌ fail, ⚪ known package manager limitation, ➖ not applicable)\n\n'
|
||||
md += `| package manager | layout | ${cols.join(' | ')} |\n|---|---|${cols.map(() => '---').join('|')}|\n`
|
||||
for (const pm of pms) {
|
||||
for (const s of SCENARIOS) {
|
||||
const cells = cols.map((c) => {
|
||||
const r = rows.find((r) => r.pm === pm && r.s === s && r.mode.id === c)
|
||||
return r ? ICONS[r.verdict] : ''
|
||||
})
|
||||
if (cells.some(Boolean))
|
||||
md += `| ${pm.id} (${pm.resolved ?? pm.kind}) | ${s.id} | ${cells.join(' | ')} |\n`
|
||||
}
|
||||
}
|
||||
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md)
|
||||
}
|
||||
|
||||
if (opts.keep) console.log(`\nfixtures kept in ${WORK}`)
|
||||
else fs.rmSync(WORK, { recursive: true, force: true })
|
||||
process.exit(failures.length || setupErrors.length ? 1 : 0)
|
||||
Generated
+71
-1034
File diff suppressed because it is too large.
Load diff
@@ -45,7 +45,7 @@ Tauri currently supports development and distribution on the following platforms
|
||||
|
||||
| Platform | Versions |
|
||||
| :--------- | :-------------------------------------------------------------------------------------------------------------- |
|
||||
| Windows | 7 and above |
|
||||
| Windows | 10 and above |
|
||||
| macOS | 10.15 and above |
|
||||
| Linux | webkit2gtk 4.0 for Tauri v1 (for example Ubuntu 18.04). webkit2gtk 4.1 for Tauri v2 (for example Ubuntu 22.04). |
|
||||
| iOS/iPadOS | 9 and above |
|
||||
|
||||
@@ -70,6 +70,22 @@
|
||||
### Breaking Changes
|
||||
|
||||
- [`1e5ba7b53`](https://www.github.com/tauri-apps/tauri/commit/1e5ba7b53dfb3da97f372b646f15853e5ba0e1a8) ([#15985](https://www.github.com/tauri-apps/tauri/pull/15985)) The CEF runtime is now detected through the `tauri-runtime-cef` dependency of the application (the `DEP_TAURI_RUNTIME_CEF_RUNTIME` env var it exports to the app's build script) instead of the removed `cef` feature of `tauri`.
|
||||
|
||||
## [2.7.3]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`e140a1c8f`](https://www.github.com/tauri-apps/tauri/commit/e140a1c8f958ecb820c43effd99468ccb25b63c3) ([#16280](https://www.github.com/tauri-apps/tauri/pull/16280)) Build-time ACL validation now suggests the closest known identifier when a capability references an unknown permission that looks like a typo of an existing one, for example `Permission fs:allow-read-text-fil not found (did you mean "fs:allow-read-text-file"?), expected one of ...`. Typos in the plugin namespace and a missing `allow-` prefix are also covered; unrelated identifiers keep the previous message.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`225fb205a`](https://www.github.com/tauri-apps/tauri/commit/225fb205a9b13e87bb71b599d896cdd9dbe7a672) ([#16247](https://www.github.com/tauri-apps/tauri/pull/16247)) Switch back to `static_vcruntime` crate, this fixes a linking error when the crate using `tauri_build::build` is used as library.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-codegen@2.7.3`
|
||||
|
||||
## [2.7.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -38,6 +38,8 @@ semver = "1"
|
||||
dirs = "7"
|
||||
glob = "0.3"
|
||||
schemars = { version = "1", features = ["preserve_order"] }
|
||||
static_vcruntime = "3.0"
|
||||
strsim = "0.11"
|
||||
|
||||
[features]
|
||||
default = ["config-json"]
|
||||
|
||||
@@ -410,8 +410,12 @@ fn validate_capabilities(
|
||||
}
|
||||
}
|
||||
|
||||
let hint = suggest_identifier(permission_id.get(), &available_permissions)
|
||||
.map(|suggestion| format!(" (did you mean \"{suggestion}\"?)"))
|
||||
.unwrap_or_default();
|
||||
|
||||
anyhow::bail!(
|
||||
"Permission {} not found, expected one of {}",
|
||||
"Permission {} not found{hint}, expected one of {}",
|
||||
permission_id.get(),
|
||||
available_permissions.join(", ")
|
||||
);
|
||||
@@ -495,3 +499,294 @@ pub fn build(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Minimum [`strsim::jaro_winkler`] similarity for a candidate to be suggested as a
|
||||
/// "did you mean" hint.
|
||||
///
|
||||
/// Matches the threshold used by the application-category parser in `tauri-bundler`,
|
||||
/// so the two suggestion surfaces behave consistently.
|
||||
const SUGGESTION_CONFIDENCE_THRESHOLD: f64 = 0.8;
|
||||
|
||||
/// Returns whether two components match exactly or with one edit or adjacent transposition.
|
||||
fn components_match(input: &str, candidate: &str) -> bool {
|
||||
input == candidate
|
||||
|| (input.chars().count().max(candidate.chars().count()) >= 4
|
||||
&& strsim::osa_distance(input, candidate) <= 1)
|
||||
}
|
||||
|
||||
/// Splits an identifier into its namespace (everything before the last `:`) and its base.
|
||||
fn split_identifier(identifier: &str) -> (Option<&str>, &str) {
|
||||
match identifier.rsplit_once(':') {
|
||||
Some((namespace, base)) => (Some(namespace), base),
|
||||
None => (None, identifier),
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks the `-` separated components of two identifier bases.
|
||||
fn base_components_match(input_parts: &[&str], candidate_base: &str) -> bool {
|
||||
candidate_base.split('-').count() == input_parts.len()
|
||||
&& input_parts
|
||||
.iter()
|
||||
.zip(candidate_base.split('-'))
|
||||
.all(|(a, b)| components_match(a, b))
|
||||
}
|
||||
|
||||
/// Checks whether a candidate namespace is acceptable for the rejected identifier's namespace.
|
||||
///
|
||||
/// The namespace must match exactly, unless no candidate uses it (an unknown plugin or a typo in
|
||||
/// its name), in which case each `:` separated component may contain a single typo.
|
||||
fn namespaces_match(
|
||||
input_namespace: Option<&str>,
|
||||
candidate_namespace: Option<&str>,
|
||||
input_namespace_known: bool,
|
||||
) -> bool {
|
||||
match (input_namespace, candidate_namespace) {
|
||||
(input, candidate) if input == candidate => true,
|
||||
(Some(input), Some(candidate)) if !input_namespace_known => {
|
||||
input.split(':').count() == candidate.split(':').count()
|
||||
&& input
|
||||
.split(':')
|
||||
.zip(candidate.split(':'))
|
||||
.all(|(a, b)| components_match(a, b))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the identifier from `candidates` that most closely resembles `input`, if any
|
||||
/// is similar enough to be worth suggesting.
|
||||
///
|
||||
/// Used to turn a rejected permission identifier into an actionable build error. Returns
|
||||
/// `None` when no candidate clears [`SUGGESTION_CONFIDENCE_THRESHOLD`], so unrelated typos
|
||||
/// do not get a misleading hint.
|
||||
fn suggest_identifier(input: &str, candidates: &[String]) -> Option<String> {
|
||||
let (input_namespace, input_base) = split_identifier(input);
|
||||
let input_namespace_known = candidates
|
||||
.iter()
|
||||
.any(|candidate| split_identifier(candidate).0 == input_namespace);
|
||||
|
||||
// Also try the input with the `allow-` prefix, which is easy to forget.
|
||||
let mut queries = vec![(input.to_string(), input_base.to_string())];
|
||||
if !input_base.starts_with("allow-") && !input_base.starts_with("deny-") {
|
||||
let base = format!("allow-{input_base}");
|
||||
let query = match input_namespace {
|
||||
Some(namespace) => format!("{namespace}:{base}"),
|
||||
None => base.clone(),
|
||||
};
|
||||
queries.push((query, base));
|
||||
}
|
||||
|
||||
let mut best_confidence = 0.0;
|
||||
let mut best_match: Option<&String> = None;
|
||||
|
||||
for (query, query_base) in &queries {
|
||||
let query_parts: Vec<&str> = query_base.split('-').collect();
|
||||
|
||||
for candidate in candidates {
|
||||
if candidate == input {
|
||||
continue;
|
||||
}
|
||||
|
||||
let (candidate_namespace, candidate_base) = split_identifier(candidate);
|
||||
|
||||
if !namespaces_match(input_namespace, candidate_namespace, input_namespace_known) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if !base_components_match(&query_parts, candidate_base) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let confidence = strsim::jaro_winkler(query, candidate);
|
||||
let edit_similarity = strsim::normalized_levenshtein(query, candidate);
|
||||
|
||||
// Jaro-Winkler alone overvalues shared prefixes.
|
||||
if confidence >= SUGGESTION_CONFIDENCE_THRESHOLD
|
||||
&& edit_similarity >= SUGGESTION_CONFIDENCE_THRESHOLD
|
||||
&& confidence > best_confidence
|
||||
{
|
||||
best_confidence = confidence;
|
||||
best_match = Some(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
best_match.cloned()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::suggest_identifier;
|
||||
|
||||
fn candidates() -> Vec<String> {
|
||||
[
|
||||
"fs:allow-read-text-file",
|
||||
"fs:allow-write-text-file",
|
||||
"fs:default",
|
||||
"core:default",
|
||||
"core:event:allow-listen",
|
||||
]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_on_missing_trailing_character() {
|
||||
assert_eq!(
|
||||
suggest_identifier("fs:allow-read-text-fil", &candidates()).as_deref(),
|
||||
Some("fs:allow-read-text-file")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_nearest_candidate_for_typo() {
|
||||
assert_eq!(
|
||||
suggest_identifier("fs:allow-read-text-fiel", &candidates()).as_deref(),
|
||||
Some("fs:allow-read-text-file")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn picks_the_closest_candidate() {
|
||||
let candidates: Vec<String> = ["core:event:allow-listen", "core:event:allow-unlisten"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
suggest_identifier("core:event:allow-listenx", &candidates).as_deref(),
|
||||
Some("core:event:allow-listen")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_read_write_mismatch() {
|
||||
let c: Vec<String> = ["fs:allow-write-text-file"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(suggest_identifier("fs:allow-read-text-file", &c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_listen_unlisten_mismatch() {
|
||||
let c: Vec<String> = ["core:event:allow-unlisten"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(suggest_identifier("core:event:allow-listen", &c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_transposed_letters_in_action() {
|
||||
let c: Vec<String> = ["core:event:allow-listen"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
suggest_identifier("core:event:allow-lisetn", &c).as_deref(),
|
||||
Some("core:event:allow-listen")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_different_hyphenation() {
|
||||
let c: Vec<String> = ["fs:allow-read-text-file"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(suggest_identifier("fs:allow-readtext-file", &c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_action_components_must_match_exactly() {
|
||||
let c: Vec<String> = ["core:window:allow-in"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(suggest_identifier("core:window:allow-on", &c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_totally_different_identifier() {
|
||||
assert_eq!(suggest_identifier("totally-unrelated", &candidates()), None);
|
||||
assert_eq!(suggest_identifier("", &candidates()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_incomplete_permission() {
|
||||
assert_eq!(suggest_identifier("fs:allow", &candidates()), None);
|
||||
assert_eq!(suggest_identifier("fs:allow-foo", &candidates()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_for_different_namespace() {
|
||||
assert_eq!(
|
||||
suggest_identifier("xx:allow-read-text-file", &candidates()),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_for_namespace_typo() {
|
||||
let c: Vec<String> = ["core:window:allow-set-title", "dialog:default"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
suggest_identifier("core:windw:allow-set-title", &c).as_deref(),
|
||||
Some("core:window:allow-set-title")
|
||||
);
|
||||
assert_eq!(
|
||||
suggest_identifier("dialgo:default", &c).as_deref(),
|
||||
Some("dialog:default")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_namespace_typo_suggestion_when_namespace_exists() {
|
||||
let c: Vec<String> = ["core:window:allow-close", "core:webview:allow-print"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(suggest_identifier("core:window:allow-print", &c), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suggests_missing_allow_prefix() {
|
||||
let c: Vec<String> = [
|
||||
"core:window:allow-set-title",
|
||||
"core:window:deny-set-title",
|
||||
"allow-echo",
|
||||
]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
suggest_identifier("core:window:set-title", &c).as_deref(),
|
||||
Some("core:window:allow-set-title")
|
||||
);
|
||||
assert_eq!(
|
||||
suggest_identifier("core:window:set-titel", &c).as_deref(),
|
||||
Some("core:window:allow-set-title")
|
||||
);
|
||||
assert_eq!(
|
||||
suggest_identifier("echo", &c).as_deref(),
|
||||
Some("allow-echo")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_suggestion_without_candidates() {
|
||||
assert_eq!(suggest_identifier("fs:allow-read", &[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_match_is_never_suggested() {
|
||||
assert_eq!(
|
||||
suggest_identifier("fs:default", &candidates()),
|
||||
None,
|
||||
"an exact match is not a misspelling, so nothing should be suggested"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,6 @@ mod acl;
|
||||
mod codegen;
|
||||
mod manifest;
|
||||
mod mobile;
|
||||
mod static_vcruntime;
|
||||
|
||||
#[cfg(feature = "codegen")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "codegen")))]
|
||||
@@ -1037,7 +1036,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
}
|
||||
}
|
||||
"msvc" if static_vc_runtime => {
|
||||
static_vcruntime::build();
|
||||
static_vcruntime::metabuild();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// taken from <https://github.com/ChrisDenton/static_vcruntime/>
|
||||
// we're not using static_vcruntime directly because we want this for debug builds too
|
||||
|
||||
use std::{env, fs, io::Write, path::Path};
|
||||
|
||||
pub fn build() {
|
||||
override_msvcrt_lib();
|
||||
|
||||
// Disable conflicting libraries that aren't hard coded by Rust
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:libvcruntimed.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:vcruntime.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:vcruntimed.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:libcmtd.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:msvcrt.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:msvcrtd.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:libucrt.lib");
|
||||
println!("cargo:rustc-link-arg=/NODEFAULTLIB:libucrtd.lib");
|
||||
// Set the libraries we want.
|
||||
println!("cargo:rustc-link-arg=/DEFAULTLIB:libcmt.lib");
|
||||
println!("cargo:rustc-link-arg=/DEFAULTLIB:libvcruntime.lib");
|
||||
println!("cargo:rustc-link-arg=/DEFAULTLIB:ucrt.lib");
|
||||
}
|
||||
|
||||
/// Override the hard-coded msvcrt.lib by replacing it with a (mostly) empty object file.
|
||||
fn override_msvcrt_lib() {
|
||||
// Get the right machine type for the empty library.
|
||||
let arch = std::env::var("CARGO_CFG_TARGET_ARCH");
|
||||
let machine: &[u8] = if arch.as_deref() == Ok("x86_64") {
|
||||
&[0x64, 0x86]
|
||||
} else if arch.as_deref() == Ok("x86") {
|
||||
&[0x4C, 0x01]
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let bytes: &[u8] = &[
|
||||
1, 0, 94, 3, 96, 98, 60, 0, 0, 0, 1, 0, 0, 0, 0, 0, 132, 1, 46, 100, 114, 101, 99, 116, 118,
|
||||
101, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 60, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
10, 16, 0, 46, 100, 114, 101, 99, 116, 118, 101, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 4, 0, 0, 0,
|
||||
];
|
||||
|
||||
// Write the empty "msvcrt.lib" to the output directory.
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let path = Path::new(&out_dir).join("msvcrt.lib");
|
||||
let f = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create_new(true)
|
||||
.open(path);
|
||||
if let Ok(mut f) = f {
|
||||
f.write_all(machine).unwrap();
|
||||
f.write_all(bytes).unwrap();
|
||||
}
|
||||
// Add the output directory to the native library path.
|
||||
println!("cargo:rustc-link-search=native={out_dir}");
|
||||
}
|
||||
@@ -58,6 +58,21 @@
|
||||
|
||||
- Upgraded to `tauri-utils@3.0.0-alpha.0`
|
||||
- Upgraded to `tauri-macos-sign@3.0.0-alpha.0`
|
||||
|
||||
## [2.10.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`5a6149898`](https://www.github.com/tauri-apps/tauri/commit/5a614989840d3176775cbdb8731ab7ca328845e2) ([#16227](https://www.github.com/tauri-apps/tauri/pull/16227)) `output_ok` used to throw away the stderr it captured, so a failing bundler script surfaced as a bare "failed to run" with no diagnosis (#3055). The stderr (or stdout, for tools such as WiX and signtool that report errors there) is now part of the error, and when the DMG script dies because macOS denied the terminal permission to control Finder, the error says where to enable it: System Settings > Privacy & Security > Automation.
|
||||
- [`d62e5d9c0`](https://www.github.com/tauri-apps/tauri/commit/d62e5d9c0878b6fb327543cc9be50aa1696ee23c) ([#16184](https://www.github.com/tauri-apps/tauri/pull/16184)) Fix NSIS installer leaving `uninstall.exe` and the installation directory behind when uninstalling an existing installation from the installer.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-macos-sign@2.4.2`
|
||||
- [`5a2953e55`](https://www.github.com/tauri-apps/tauri/commit/5a2953e555b4444bed68bd5324ea13ad85a6c06e) ([#16237](https://www.github.com/tauri-apps/tauri/pull/16237)) Updated NSIS to 3.13
|
||||
- [`55883af3f`](https://www.github.com/tauri-apps/tauri/commit/55883af3f40b569d57ff9ae4bfc0b1db5845a7d9) ([#15639](https://www.github.com/tauri-apps/tauri/pull/15639)) Updated `rpm-rs` crate to `v0.30` which should improve build times for .rpm bundles.
|
||||
|
||||
## [2.10.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -64,7 +64,7 @@ ar = "0.9"
|
||||
md5 = "0.8"
|
||||
# TODO: xz-compression's `lzma` conflicts with apple-codesign's `xz`
|
||||
# Sequoia preserves OpenPGP signing without rPGP's vulnerable draft ML-DSA dependency.
|
||||
rpm = { version = "0.28.0", default-features = false, features = [
|
||||
rpm = { version = "0.30", default-features = false, features = [
|
||||
"gzip-compression",
|
||||
"payload",
|
||||
"signature-sequoia",
|
||||
|
||||
@@ -23,6 +23,17 @@ pub struct Bundled {
|
||||
pub app: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
// bundle_dmg.sh drives Finder through osascript, and macOS denies that unless
|
||||
// the terminal has Finder automation permission (#3055). Match the error code
|
||||
// (errAEEventNotPermitted), since the message itself is localized.
|
||||
fn finder_permission_hint(stderr: &str) -> Option<&'static str> {
|
||||
stderr
|
||||
.contains("(-1743)")
|
||||
.then_some(
|
||||
"the terminal needs permission to control Finder: System Settings > Privacy & Security > Automation",
|
||||
)
|
||||
}
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the DMG was created.
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Bundled> {
|
||||
@@ -181,11 +192,18 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
log::info!(action = "Running"; "bundle_dmg.sh");
|
||||
|
||||
// execute the bundle script
|
||||
bundle_dmg_cmd
|
||||
let result = bundle_dmg_cmd
|
||||
.current_dir(bundle_dir.clone())
|
||||
.args(vec![dmg_name.as_str(), bundle_file_name.as_str()])
|
||||
.output_ok()
|
||||
.context("error running bundle_dmg.sh")?;
|
||||
.output_ok();
|
||||
let hint = result
|
||||
.as_ref()
|
||||
.err()
|
||||
.and_then(|err| finder_permission_hint(&err.to_string()));
|
||||
result.with_context(|| match hint {
|
||||
Some(hint) => format!("error running bundle_dmg.sh ({hint})"),
|
||||
None => "error running bundle_dmg.sh".to_string(),
|
||||
})?;
|
||||
|
||||
fs::rename(bundle_dir.join(dmg_name), dmg_path.clone())?;
|
||||
|
||||
@@ -211,3 +229,16 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
app: app_bundle_paths,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::finder_permission_hint;
|
||||
|
||||
#[test]
|
||||
fn names_the_finder_permission_fix() {
|
||||
let stderr = "bundle_dmg.sh: Failed running AppleScript\nexecution error: Not authorized to send Apple events to Finder. (-1743)\n";
|
||||
assert!(finder_permission_hint(stderr).is_some_and(|hint| hint.contains("Automation")));
|
||||
assert!(finder_permission_hint("execution error: Finder n'a pas autorisé (-1743)").is_some());
|
||||
assert!(finder_permission_hint("some other failure").is_none());
|
||||
}
|
||||
}
|
||||
@@ -382,6 +382,15 @@ Function PageLeaveReinstall
|
||||
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; The NSIS uninstaller was run in place (`_?=`), so it cannot delete itself
|
||||
; and the install directory, clean them up here once the uninstall succeeded
|
||||
; https://nsis.sourceforge.io/Docs/Chapter3.html#:~:text=path%20contains%20spaces.-,3.2.3%20Examples,-installer.exe%20/NCRC
|
||||
${If} $WixMode = 0
|
||||
${AndIf} $4 != ""
|
||||
Delete "$4\uninstall.exe"
|
||||
RMDir "$4"
|
||||
${EndIf}
|
||||
reinst_done:
|
||||
FunctionEnd
|
||||
|
||||
|
||||
@@ -36,9 +36,10 @@ use std::{
|
||||
// URLS for the NSIS toolchain.
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_URL: &str =
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip";
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.13/nsis-3.13.zip";
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_SHA1: &str = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D";
|
||||
const NSIS_SHA1: &str = "DB14F8AF2EA346B786C6AE19343F61AAF5E09B8E";
|
||||
const NSIS_EXE_SHA1: &str = "6D45D4E8CCF18FE1FDAC9164058F356102A2ECF2";
|
||||
const NSIS_TAURI_UTILS_URL: &str = "https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.3/nsis_tauri_utils.dll";
|
||||
const NSIS_TAURI_UTILS_SHA1: &str = "75197FEE3C6A814FE035788D1C34EAD39349B860";
|
||||
|
||||
@@ -94,6 +95,21 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
|
||||
log::warn!("NSIS directory is missing some files. Recreating it.");
|
||||
std::fs::remove_dir_all(&nsis_toolset_path)?;
|
||||
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
|
||||
} else if let Err(e) = verify_file_hash(
|
||||
nsis_toolset_path.join("NSIS.exe"),
|
||||
NSIS_EXE_SHA1,
|
||||
HashAlgorithm::Sha1,
|
||||
) {
|
||||
log::warn!(
|
||||
"NSIS.exe is {}. Recreating it.",
|
||||
if matches!(e, Error::HashError) {
|
||||
"outdated"
|
||||
} else {
|
||||
"missing"
|
||||
}
|
||||
);
|
||||
std::fs::remove_dir_all(&nsis_toolset_path)?;
|
||||
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
|
||||
} else {
|
||||
let mismatched = NSIS_REQUIRED_FILES_HASH
|
||||
.iter()
|
||||
@@ -127,7 +143,7 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
|
||||
let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgorithm::Sha1)?;
|
||||
log::info!("extracting NSIS");
|
||||
crate::utils::http_utils::extract_zip(&data, _tauri_tools_path)?;
|
||||
fs::rename(_tauri_tools_path.join("nsis-3.11"), nsis_toolset_path)?;
|
||||
fs::rename(_tauri_tools_path.join("nsis-3.13"), nsis_toolset_path)?;
|
||||
}
|
||||
|
||||
// download additional plugins
|
||||
|
||||
@@ -57,7 +57,7 @@ impl CommandExt for Command {
|
||||
let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
|
||||
let stdout_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stdout_lines_ = stdout_lines.clone();
|
||||
std::thread::spawn(move || {
|
||||
let stdout_thread = std::thread::spawn(move || {
|
||||
let mut line = String::new();
|
||||
let mut lines = stdout_lines_.lock().unwrap();
|
||||
loop {
|
||||
@@ -76,7 +76,7 @@ impl CommandExt for Command {
|
||||
let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
|
||||
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stderr_lines_ = stderr_lines.clone();
|
||||
std::thread::spawn(move || {
|
||||
let stderr_thread = std::thread::spawn(move || {
|
||||
let mut line = String::new();
|
||||
let mut lines = stderr_lines_.lock().unwrap();
|
||||
loop {
|
||||
@@ -93,6 +93,8 @@ impl CommandExt for Command {
|
||||
});
|
||||
|
||||
let status = child.wait()?;
|
||||
let _ = stdout_thread.join();
|
||||
let _ = stderr_thread.join();
|
||||
let output = Output {
|
||||
status,
|
||||
stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
|
||||
@@ -102,9 +104,15 @@ impl CommandExt for Command {
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
// some tools (WiX, signtool) report their errors on stdout
|
||||
let diagnosis = if output.stderr.iter().all(u8::is_ascii_whitespace) {
|
||||
&output.stdout
|
||||
} else {
|
||||
&output.stderr
|
||||
};
|
||||
Err(crate::Error::CommandFailed {
|
||||
command: program,
|
||||
error: std::io::Error::other(String::from_utf8_lossy(&output.stderr).to_string()),
|
||||
error: std::io::Error::other(String::from_utf8_lossy(diagnosis).trim_end().to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -116,7 +124,25 @@ mod tests {
|
||||
|
||||
use tauri_utils::resources::resource_relpath;
|
||||
|
||||
use super::is_retina;
|
||||
use super::{CommandExt, is_retina};
|
||||
|
||||
#[test]
|
||||
fn output_ok_keeps_stderr_in_the_error() {
|
||||
let err = std::process::Command::new("sh")
|
||||
.args(["-c", "echo the-diagnosis >&2; exit 1"])
|
||||
.output_ok()
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("the-diagnosis"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_ok_falls_back_to_stdout_without_stderr() {
|
||||
let err = std::process::Command::new("sh")
|
||||
.args(["-c", "echo the-diagnosis; exit 1"])
|
||||
.output_ok()
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("the-diagnosis"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retina_icon_paths() {
|
||||
|
||||
@@ -87,6 +87,25 @@
|
||||
- The CEF files, code signing entitlements and macOS dev flow are only used when the app uses CEF.
|
||||
- Nothing runtime-specific is done for other runtimes.
|
||||
- The app and plugin templates add the `tauri-runtime-wry` dependency and select it with `tauri::Builder::default().runtime(tauri_runtime_wry::Wry::default())`.
|
||||
|
||||
## [2.12.3]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`107f93ae0`](https://www.github.com/tauri-apps/tauri/commit/107f93ae0b8bbcd3eec4f8af848034272a67b849) ([#16259](https://www.github.com/tauri-apps/tauri/pull/16259)) Standalone Linux ARM64 GNU and musl CLI archives are now included in releases.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`c95be4615`](https://www.github.com/tauri-apps/tauri/commit/c95be4615a432b1d12409d3684c960e8aa718ada) ([#16287](https://www.github.com/tauri-apps/tauri/pull/16287)) Fix `tauri ios xcode-script` failing to find the Tauri project when Xcode inherits package manager environment variables from `tauri ios dev --open`, or when pnpm runs the CLI directly from the `gen/apple` folder.
|
||||
- [`d62e5d9c0`](https://www.github.com/tauri-apps/tauri/commit/d62e5d9c0878b6fb327543cc9be50aa1696ee23c) ([#16184](https://www.github.com/tauri-apps/tauri/pull/16184)) Fix NSIS installer leaving `uninstall.exe` and the installation directory behind when uninstalling an existing installation from the installer.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-macos-sign@2.4.2`
|
||||
- Upgraded to `tauri-bundler@2.10.3`
|
||||
- [`5a2953e55`](https://www.github.com/tauri-apps/tauri/commit/5a2953e555b4444bed68bd5324ea13ad85a6c06e) ([#16237](https://www.github.com/tauri-apps/tauri/pull/16237)) Updated NSIS to 3.13
|
||||
|
||||
## [2.12.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -17,7 +17,7 @@ use object::{Object, ObjectSymbol};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{current_dir, set_current_dir, var, var_os},
|
||||
env::{current_dir, set_current_dir},
|
||||
ffi::OsStr,
|
||||
fs::read_to_string,
|
||||
io::Read,
|
||||
@@ -65,28 +65,19 @@ pub fn command(options: Options) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
let process_path = std::env::current_exe().unwrap_or_default();
|
||||
|
||||
// `xcode-script` is ran from the `gen/apple` folder when not using NPM/yarn/pnpm/deno.
|
||||
// so we must change working directory to the src-tauri folder to resolve the tauri dir
|
||||
// additionally, bun@<1.2 does not modify the current working directory, so it is also runs this script from `gen/apple`
|
||||
// bun@>1.2 now actually moves the CWD to the package root so we shouldn't modify CWD in that case
|
||||
// see https://bun.sh/blog/bun-v1.2#bun-run-uses-the-correct-directory
|
||||
if (var_os("npm_lifecycle_event").is_none()
|
||||
&& var_os("PNPM_PACKAGE_NAME").is_none()
|
||||
&& process_path.file_stem().unwrap_or_default() != "deno")
|
||||
|| var("npm_config_user_agent")
|
||||
.is_ok_and(|agent| agent.starts_with("bun/1.0") || agent.starts_with("bun/1.1"))
|
||||
{
|
||||
set_current_dir(
|
||||
current_dir()
|
||||
.context("failed to resolve current directory")?
|
||||
.parent()
|
||||
.unwrap()
|
||||
.parent()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
// Xcode runs `xcode-script` from `gen/apple`. Most package managers move the working directory to the
|
||||
// package root, but some don't (cargo, bun@<1.2, pnpm@12.3 outside a package), so if the tauri dir
|
||||
// can't be resolved and we're in `gen/apple`, move up to the src-tauri folder.
|
||||
// Package manager env vars are unreliable here (Xcode inherits them from `tauri ios dev --open`).
|
||||
let cwd = current_dir().context("failed to resolve current directory")?;
|
||||
if cwd.ends_with("gen/apple") && crate::helpers::app_paths::resolve_tauri_dir().is_none() {
|
||||
let tauri_dir = cwd.parent().and_then(Path::parent).unwrap();
|
||||
set_current_dir(tauri_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to change current directory to {}",
|
||||
tauri_dir.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
let dirs = crate::helpers::app_paths::resolve_dirs();
|
||||
|
||||
@@ -51,6 +51,13 @@
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@3.0.0-alpha.0`
|
||||
|
||||
## [2.7.3]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
|
||||
## [2.7.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -18,6 +18,17 @@
|
||||
|
||||
- [`c9277f3c0`](https://www.github.com/tauri-apps/tauri/commit/c9277f3c0c24518a7ab7d7d1f2489e004b1597f7) Set MSRV to 1.95.
|
||||
- [`19929799f`](https://www.github.com/tauri-apps/tauri/commit/19929799f42398a6e85adb00ae02f2e7fe46d214) First v3 alpha release!
|
||||
|
||||
## [2.4.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`b9487627b`](https://www.github.com/tauri-apps/tauri/commit/b9487627b17f07c70db68d890a560112bdbf3505) ([#15844](https://www.github.com/tauri-apps/tauri/pull/15844)) Fixed importing a signing certificate failing with `MAC verification failed during PKCS12 import` for p12 files that use modern PBES2/AES encryption. The certificate format is now detected and passed to `security import` explicitly (PKCS#12 or a PEM bundle), and import failures explain the expected certificate format and password.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [`5a3cbfcdc`](https://www.github.com/tauri-apps/tauri/commit/5a3cbfcdcaad82185ce2cc9f4d246c78e144c78c) ([#16279](https://www.github.com/tauri-apps/tauri/pull/16279)) **Breaking Change:** Temporarily inlined the code we need from apple-codesign-rs until its next release to unblock important dependency updates. Technically this is a breaking change to the `CertificateProfile` enum re-export and the `Error::FailedToCreateSelfSignedCertificate` enum variant.
|
||||
|
||||
## [2.4.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
name = "tauri-macos-sign"
|
||||
version = "3.0.0-alpha.2"
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
license = "(Apache-2.0 OR MIT) AND MPL-2.0"
|
||||
keywords = ["codesign", "signing", "macos", "ios", "tauri"]
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
description = "Code signing utilities for macOS and iOS apps"
|
||||
@@ -23,10 +23,18 @@ plist = "1"
|
||||
rand = "0.10"
|
||||
dirs = "7"
|
||||
log = { version = "0.4.21", features = ["kv"] }
|
||||
apple-codesign = { version = "0.29", default-features = false }
|
||||
# apple-codesign = { version = "0.29", default-features = false }
|
||||
chrono = "0.4"
|
||||
p12 = "0.6"
|
||||
base64 = "0.23"
|
||||
|
||||
# apple-codesign
|
||||
rsa = "0.9"
|
||||
rand08 = { package = "rand", version = "0.8" }
|
||||
hex = "0.4"
|
||||
bcder = "0.7"
|
||||
bytes = "1"
|
||||
pkcs8 = { version = "0.10.2", features = ["alloc", "std"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,19 @@
|
||||
# apple_codesign
|
||||
|
||||
This directory contains code vendored from the [`apple-codesign`](https://crates.io/crates/apple-codesign)
|
||||
crate, version 0.29.0, part of [apple-platform-rs](https://github.com/indygreg/apple-platform-rs).
|
||||
Copyright Gregory Szorc and contributors.
|
||||
|
||||
Only the self-signed code signing certificate generation used by `tauri-macos-sign` was kept,
|
||||
the rest of the crate was removed. The upstream crate has not been released since 0.29.0 and
|
||||
depending on it blocked dependency updates elsewhere in Tauri.
|
||||
|
||||
## License
|
||||
|
||||
The files vendored from upstream (`certificate.rs` and `error.rs`) are licensed under the
|
||||
[Mozilla Public License 2.0](https://mozilla.org/MPL/2.0/), as in the original crate, and each
|
||||
keeps its original MPL-2.0 notice. Modifications made to those files are also MPL-2.0.
|
||||
|
||||
`mod.rs` and the rest of `tauri-macos-sign` are licensed under Apache-2.0 OR MIT. MPL-2.0
|
||||
allows its files to be combined with code under other licenses in a larger work, so the crate
|
||||
as a whole is `(Apache-2.0 OR MIT) AND MPL-2.0`.
|
||||
@@ -0,0 +1,870 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// Vendored from apple-codesign 0.29.0 <https://github.com/indygreg/apple-platform-rs>,
|
||||
// Copyright Gregory Szorc and contributors. Modified by the Tauri Programme
|
||||
// within The Commons Conservancy. See README.md in this directory.
|
||||
|
||||
//! Functionality related to certificates.
|
||||
|
||||
use {
|
||||
super::error::AppleCodesignError,
|
||||
bcder::{
|
||||
ConstOid, Oid,
|
||||
encode::{PrimitiveContent, Values},
|
||||
},
|
||||
bytes::Bytes,
|
||||
pkcs8::EncodePrivateKey,
|
||||
std::{
|
||||
fmt::{Display, Formatter},
|
||||
str::FromStr,
|
||||
},
|
||||
x509_certificate::{
|
||||
CapturedX509Certificate, InMemorySigningKeyPair, KeyAlgorithm, X509CertificateBuilder,
|
||||
certificate::KeyUsage, rfc4519::OID_COUNTRY_NAME,
|
||||
},
|
||||
};
|
||||
|
||||
/// Extended Key Usage extension.
|
||||
///
|
||||
/// 2.5.29.37
|
||||
const OID_EXTENDED_KEY_USAGE: ConstOid = Oid(&[85, 29, 37]);
|
||||
|
||||
/// Extended Key Usage purpose for code signing.
|
||||
///
|
||||
/// 1.3.6.1.5.5.7.3.3
|
||||
const OID_EKU_PURPOSE_CODE_SIGNING: ConstOid = Oid(&[43, 6, 1, 5, 5, 7, 3, 3]);
|
||||
|
||||
/// Extended Key Usage for purpose of `Safari Developer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.4.8
|
||||
const OID_EKU_PURPOSE_SAFARI_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 8]);
|
||||
|
||||
/// Extended Key Usage for purpose of `3rd Party Mac Developer Installer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.4.9
|
||||
const OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 9]);
|
||||
|
||||
/// Extended Key Usage for purpose of `Developer ID Installer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.4.13
|
||||
const OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 4, 13]);
|
||||
|
||||
/// Extension for `Apple Signing`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.1
|
||||
const OID_EXTENSION_APPLE_SIGNING: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 1]);
|
||||
|
||||
/// Extension for `iPhone Developer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.2
|
||||
const OID_EXTENSION_IPHONE_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 2]);
|
||||
|
||||
/// Extension for `Apple iPhone OS Application Signing`
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.3
|
||||
const OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 3]);
|
||||
|
||||
/// Extension for `Apple Developer Certificate (Submission)`.
|
||||
///
|
||||
/// May also be referred to as `iPhone Distribution`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.4
|
||||
const OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 4]);
|
||||
|
||||
/// Extension for `Safari Developer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.5
|
||||
const OID_EXTENSION_SAFARI_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 5]);
|
||||
|
||||
/// Extension for `Apple iPhone OS VPN Signing`
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.6
|
||||
const OID_EXTENSION_IPHONE_OS_VPN_SIGNING: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 6]);
|
||||
|
||||
/// Extension for `Apple Mac App Signing (Development)`.
|
||||
///
|
||||
/// May also appear as `3rd Party Mac Developer Application`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.7
|
||||
const OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 7]);
|
||||
|
||||
/// Extension for `Apple Mac App Signing Submission`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.8
|
||||
const OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 8]);
|
||||
|
||||
/// Extension for `Mac App Store Code Signing`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.9
|
||||
const OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 9]);
|
||||
|
||||
/// Extension for `Mac App Store Installer Signing`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.10
|
||||
const OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 10]);
|
||||
|
||||
// 1.2.840.113635.100.6.1.11 is unknown.
|
||||
|
||||
/// Extension for `Mac Developer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.12
|
||||
const OID_EXTENSION_MAC_DEVELOPER: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 12]);
|
||||
|
||||
/// Extension for `Developer ID Application`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.13
|
||||
const OID_EXTENSION_DEVELOPER_ID_APPLICATION: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 13]);
|
||||
|
||||
/// Extension for `Developer ID Installer`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.14
|
||||
const OID_EXTENSION_DEVELOPER_ID_INSTALLER: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 14]);
|
||||
|
||||
// 1.2.840.113635.100.6.1.15 looks to have something to do with core OS functionality,
|
||||
// as it appears in search results for hacking Apple OS booting.
|
||||
|
||||
/// Extension for `Apple Pay Passbook Signing`
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.16
|
||||
const OID_EXTENSION_PASSBOOK_SIGNING: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 16]);
|
||||
|
||||
/// Extension for `Web Site Push Notifications Signing`
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.17
|
||||
const OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 17]);
|
||||
|
||||
/// Extension for `Developer ID Kernel`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.18
|
||||
const OID_EXTENSION_DEVELOPER_ID_KERNEL: ConstOid =
|
||||
Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 18]);
|
||||
|
||||
/// Extension for `Developer ID Date`.
|
||||
///
|
||||
/// This OID doesn't have a description in Apple tooling. But it
|
||||
/// holds a UtcDate (with hours, minutes, and seconds all set to 0) and seems to
|
||||
/// denote a date constraint to apply to validation. This is likely used
|
||||
/// to validating timestamping constrains for certificate validity.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.33
|
||||
const OID_EXTENSION_DEVELOPER_ID_DATE: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 33]);
|
||||
|
||||
/// Extension for `TestFlight`.
|
||||
///
|
||||
/// 1.2.840.113635.100.6.1.25.1
|
||||
const OID_EXTENSION_TEST_FLIGHT: ConstOid = Oid(&[42, 134, 72, 134, 247, 99, 100, 6, 1, 25, 1]);
|
||||
|
||||
/// UserID.
|
||||
///
|
||||
/// 0.9.2342.19200300.100.1.1
|
||||
pub const OID_USER_ID: ConstOid = Oid(&[9, 146, 38, 137, 147, 242, 44, 100, 1, 1]);
|
||||
|
||||
/// Describes the type of code signing that a certificate is authorized to perform.
|
||||
///
|
||||
/// Code signing certificates are issued with extended key usage (EKU) attributes
|
||||
/// denoting what that certificate will be used for. They basically say *I'm authorized
|
||||
/// to sign X*.
|
||||
///
|
||||
/// This type describes the different code signing key usages defined on Apple
|
||||
/// platforms.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum ExtendedKeyUsagePurpose {
|
||||
/// Code signing.
|
||||
CodeSigning,
|
||||
|
||||
/// Safari Developer.
|
||||
SafariDeveloper,
|
||||
|
||||
/// 3rd Party Mac Developer Installer Packaging Signing.
|
||||
///
|
||||
/// The certificate can be used to sign Mac installer packages.
|
||||
ThirdPartyMacDeveloperInstaller,
|
||||
|
||||
/// Developer ID Installer.
|
||||
DeveloperIdInstaller,
|
||||
}
|
||||
|
||||
impl ExtendedKeyUsagePurpose {
|
||||
pub fn as_oid(&self) -> ConstOid {
|
||||
match self {
|
||||
Self::CodeSigning => OID_EKU_PURPOSE_CODE_SIGNING,
|
||||
Self::SafariDeveloper => OID_EKU_PURPOSE_SAFARI_DEVELOPER,
|
||||
Self::ThirdPartyMacDeveloperInstaller => OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER,
|
||||
Self::DeveloperIdInstaller => OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExtendedKeyUsagePurpose {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExtendedKeyUsagePurpose::CodeSigning => f.write_str("Code Signing"),
|
||||
ExtendedKeyUsagePurpose::SafariDeveloper => f.write_str("Safari Developer"),
|
||||
ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller => {
|
||||
f.write_str("3rd Party Mac Developer Installer Packaging Signing")
|
||||
}
|
||||
ExtendedKeyUsagePurpose::DeveloperIdInstaller => f.write_str("Developer ID Installer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Oid> for ExtendedKeyUsagePurpose {
|
||||
type Error = AppleCodesignError;
|
||||
|
||||
fn try_from(oid: &Oid) -> Result<Self, Self::Error> {
|
||||
// Surely there is a way to use `match`. But the `Oid` type is a bit wonky.
|
||||
if oid.as_ref() == OID_EKU_PURPOSE_CODE_SIGNING.as_ref() {
|
||||
Ok(Self::CodeSigning)
|
||||
} else if oid.as_ref() == OID_EKU_PURPOSE_SAFARI_DEVELOPER.as_ref() {
|
||||
Ok(Self::SafariDeveloper)
|
||||
} else if oid.as_ref() == OID_EKU_PURPOSE_3RD_PARTY_MAC_DEVELOPER_INSTALLER.as_ref() {
|
||||
Ok(Self::ThirdPartyMacDeveloperInstaller)
|
||||
} else if oid.as_ref() == OID_EKU_PURPOSE_DEVELOPER_ID_INSTALLER.as_ref() {
|
||||
Ok(Self::DeveloperIdInstaller)
|
||||
} else {
|
||||
Err(AppleCodesignError::OidIsntCertificateAuthority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes one of the many X.509 certificate extensions found on Apple code signing certificates.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CodeSigningCertificateExtension {
|
||||
/// Apple Signing.
|
||||
///
|
||||
/// (Appears to be deprecated).
|
||||
AppleSigning,
|
||||
|
||||
/// iPhone Developer.
|
||||
IPhoneDeveloper,
|
||||
|
||||
/// Apple iPhone OS Application Signing.
|
||||
IPhoneOsApplicationSigning,
|
||||
|
||||
/// Apple Developer Certificate (Submission).
|
||||
///
|
||||
/// May also be referred to as `iPhone Distribution`.
|
||||
AppleDeveloperCertificateSubmission,
|
||||
|
||||
/// Safari Developer.
|
||||
SafariDeveloper,
|
||||
|
||||
/// Apple iPhone OS VPN Signing.
|
||||
IPhoneOsVpnSigning,
|
||||
|
||||
/// Apple Mac App Signing (Development).
|
||||
///
|
||||
/// Also known as `3rd Party Mac Developer Application`.
|
||||
AppleMacAppSigningDevelopment,
|
||||
|
||||
/// Apple Mac App Signing Submission.
|
||||
AppleMacAppSigningSubmission,
|
||||
|
||||
/// Mac App Store Code Signing.
|
||||
AppleMacAppStoreCodeSigning,
|
||||
|
||||
/// Mac App Store Installer Signing.
|
||||
AppleMacAppStoreInstallerSigning,
|
||||
|
||||
/// Mac Developer.
|
||||
MacDeveloper,
|
||||
|
||||
/// Developer ID Application.
|
||||
DeveloperIdApplication,
|
||||
|
||||
/// Developer ID Date.
|
||||
DeveloperIdDate,
|
||||
|
||||
/// Developer ID Installer.
|
||||
DeveloperIdInstaller,
|
||||
|
||||
/// Apple Pay Passbook Signing.
|
||||
ApplePayPassbookSigning,
|
||||
|
||||
/// Web Site Push Notifications Signing.
|
||||
WebsitePushNotificationSigning,
|
||||
|
||||
/// Developer ID Kernel.
|
||||
DeveloperIdKernel,
|
||||
|
||||
/// TestFlight.
|
||||
TestFlight,
|
||||
}
|
||||
|
||||
impl CodeSigningCertificateExtension {
|
||||
pub fn as_oid(&self) -> ConstOid {
|
||||
match self {
|
||||
Self::AppleSigning => OID_EXTENSION_APPLE_SIGNING,
|
||||
Self::IPhoneDeveloper => OID_EXTENSION_IPHONE_DEVELOPER,
|
||||
Self::IPhoneOsApplicationSigning => OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING,
|
||||
Self::AppleDeveloperCertificateSubmission => {
|
||||
OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION
|
||||
}
|
||||
Self::SafariDeveloper => OID_EXTENSION_SAFARI_DEVELOPER,
|
||||
Self::IPhoneOsVpnSigning => OID_EXTENSION_IPHONE_OS_VPN_SIGNING,
|
||||
Self::AppleMacAppSigningDevelopment => OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT,
|
||||
Self::AppleMacAppSigningSubmission => OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION,
|
||||
Self::AppleMacAppStoreCodeSigning => OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING,
|
||||
Self::AppleMacAppStoreInstallerSigning => OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING,
|
||||
Self::MacDeveloper => OID_EXTENSION_MAC_DEVELOPER,
|
||||
Self::DeveloperIdApplication => OID_EXTENSION_DEVELOPER_ID_APPLICATION,
|
||||
Self::DeveloperIdDate => OID_EXTENSION_DEVELOPER_ID_DATE,
|
||||
Self::DeveloperIdInstaller => OID_EXTENSION_DEVELOPER_ID_INSTALLER,
|
||||
Self::ApplePayPassbookSigning => OID_EXTENSION_PASSBOOK_SIGNING,
|
||||
Self::WebsitePushNotificationSigning => OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING,
|
||||
Self::DeveloperIdKernel => OID_EXTENSION_DEVELOPER_ID_KERNEL,
|
||||
Self::TestFlight => OID_EXTENSION_TEST_FLIGHT,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CodeSigningCertificateExtension {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CodeSigningCertificateExtension::AppleSigning => f.write_str("Apple Signing"),
|
||||
CodeSigningCertificateExtension::IPhoneDeveloper => f.write_str("iPhone Developer"),
|
||||
CodeSigningCertificateExtension::IPhoneOsApplicationSigning => {
|
||||
f.write_str("Apple iPhone OS Application Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission => {
|
||||
f.write_str("Apple Developer Certificate (Submission)")
|
||||
}
|
||||
CodeSigningCertificateExtension::SafariDeveloper => f.write_str("Safari Developer"),
|
||||
CodeSigningCertificateExtension::IPhoneOsVpnSigning => {
|
||||
f.write_str("Apple iPhone OS VPN Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningDevelopment => {
|
||||
f.write_str("Apple Mac App Signing (Development)")
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningSubmission => {
|
||||
f.write_str("Apple Mac App Signing Submission")
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppStoreCodeSigning => {
|
||||
f.write_str("Mac App Store Code Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppStoreInstallerSigning => {
|
||||
f.write_str("Mac App Store Installer Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::MacDeveloper => f.write_str("Mac Developer"),
|
||||
CodeSigningCertificateExtension::DeveloperIdApplication => {
|
||||
f.write_str("Developer ID Application")
|
||||
}
|
||||
CodeSigningCertificateExtension::DeveloperIdDate => f.write_str("Developer ID Date"),
|
||||
CodeSigningCertificateExtension::DeveloperIdInstaller => {
|
||||
f.write_str("Developer ID Installer")
|
||||
}
|
||||
CodeSigningCertificateExtension::ApplePayPassbookSigning => {
|
||||
f.write_str("Apple Pay Passbook Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::WebsitePushNotificationSigning => {
|
||||
f.write_str("Web Site Push Notifications Signing")
|
||||
}
|
||||
CodeSigningCertificateExtension::DeveloperIdKernel => f.write_str("Developer ID Kernel"),
|
||||
CodeSigningCertificateExtension::TestFlight => f.write_str("TestFlight"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Oid> for CodeSigningCertificateExtension {
|
||||
type Error = AppleCodesignError;
|
||||
|
||||
fn try_from(oid: &Oid) -> Result<Self, Self::Error> {
|
||||
// Surely there is a way to use `match`. But the `Oid` type is a bit wonky.
|
||||
let o = oid.as_ref();
|
||||
|
||||
if o == OID_EXTENSION_APPLE_SIGNING.as_ref() {
|
||||
Ok(Self::AppleSigning)
|
||||
} else if o == OID_EXTENSION_IPHONE_DEVELOPER.as_ref() {
|
||||
Ok(Self::IPhoneDeveloper)
|
||||
} else if o == OID_EXTENSION_IPHONE_OS_APPLICATION_SIGNING.as_ref() {
|
||||
Ok(Self::IPhoneOsApplicationSigning)
|
||||
} else if o == OID_EXTENSION_APPLE_DEVELOPER_CERTIFICATE_SUBMISSION.as_ref() {
|
||||
Ok(Self::AppleDeveloperCertificateSubmission)
|
||||
} else if o == OID_EXTENSION_SAFARI_DEVELOPER.as_ref() {
|
||||
Ok(Self::SafariDeveloper)
|
||||
} else if o == OID_EXTENSION_IPHONE_OS_VPN_SIGNING.as_ref() {
|
||||
Ok(Self::IPhoneOsVpnSigning)
|
||||
} else if o == OID_EXTENSION_APPLE_MAC_APP_SIGNING_DEVELOPMENT.as_ref() {
|
||||
Ok(Self::AppleMacAppSigningDevelopment)
|
||||
} else if o == OID_EXTENSION_APPLE_MAC_APP_SIGNING_SUBMISSION.as_ref() {
|
||||
Ok(Self::AppleMacAppSigningSubmission)
|
||||
} else if o == OID_EXTENSION_APPLE_MAC_APP_STORE_CODE_SIGNING.as_ref() {
|
||||
Ok(Self::AppleMacAppStoreCodeSigning)
|
||||
} else if o == OID_EXTENSION_APPLE_MAC_APP_STORE_INSTALLER_SIGNING.as_ref() {
|
||||
Ok(Self::AppleMacAppStoreInstallerSigning)
|
||||
} else if o == OID_EXTENSION_MAC_DEVELOPER.as_ref() {
|
||||
Ok(Self::MacDeveloper)
|
||||
} else if o == OID_EXTENSION_DEVELOPER_ID_APPLICATION.as_ref() {
|
||||
Ok(Self::DeveloperIdApplication)
|
||||
} else if o == OID_EXTENSION_DEVELOPER_ID_INSTALLER.as_ref() {
|
||||
Ok(Self::DeveloperIdInstaller)
|
||||
} else if o == OID_EXTENSION_PASSBOOK_SIGNING.as_ref() {
|
||||
Ok(Self::ApplePayPassbookSigning)
|
||||
} else if o == OID_EXTENSION_WEBSITE_PUSH_NOTIFICATION_SIGNING.as_ref() {
|
||||
Ok(Self::WebsitePushNotificationSigning)
|
||||
} else if o == OID_EXTENSION_DEVELOPER_ID_KERNEL.as_ref() {
|
||||
Ok(Self::DeveloperIdKernel)
|
||||
} else if o == OID_EXTENSION_DEVELOPER_ID_DATE.as_ref() {
|
||||
Ok(Self::DeveloperIdDate)
|
||||
} else if o == OID_EXTENSION_TEST_FLIGHT.as_ref() {
|
||||
Ok(Self::TestFlight)
|
||||
} else {
|
||||
Err(AppleCodesignError::OidIsntCodeSigningExtension)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes combinations of certificate extensions for Apple code signing certificates.
|
||||
///
|
||||
/// Code signing certificates contain various X.509 extensions denoting them for
|
||||
/// code signing.
|
||||
///
|
||||
/// This type represents various common extensions as used on Apple platforms.
|
||||
///
|
||||
/// Typically, you'll want to apply at most one of these extensions to a
|
||||
/// new certificate in order to mark it as compatible for code signing.
|
||||
///
|
||||
/// This type essentially encapsulates the logic for handling of different
|
||||
/// "profiles" attached to the different code signing certificates that Apple
|
||||
/// issues.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CertificateProfile {
|
||||
/// Mac Installer Distribution.
|
||||
///
|
||||
/// In `Keychain Access.app`, this might render as `3rd Party Mac Developer Installer`.
|
||||
///
|
||||
/// Certificates are marked for EKU with `3rd Party Developer Installer Package
|
||||
/// Signing`.
|
||||
///
|
||||
/// They also have the `Apple Mac App Signing (Submission)` extension.
|
||||
///
|
||||
/// Typically issued by `Apple Worldwide Developer Relations Certificate
|
||||
/// Authority`.
|
||||
MacInstallerDistribution,
|
||||
|
||||
/// Apple Distribution.
|
||||
///
|
||||
/// Certificates are marked for EKU with `Code Signing`. They also have
|
||||
/// extensions `Apple Mac App Signing (Development)` and
|
||||
/// `Apple Developer Certificate (Submission)`.
|
||||
///
|
||||
/// Typically issued by `Apple Worldwide Developer Relations Certificate Authority`.
|
||||
AppleDistribution,
|
||||
|
||||
/// Apple Development.
|
||||
///
|
||||
/// Certificates are marked for EKU with `Code Signing`. They also have
|
||||
/// extensions `Apple Developer Certificate (Development)` and
|
||||
/// `Mac Developer`.
|
||||
///
|
||||
/// Typically issued by `Apple Worldwide Developer Relations Certificate
|
||||
/// Authority`.
|
||||
AppleDevelopment,
|
||||
|
||||
/// Developer ID Application.
|
||||
///
|
||||
/// Certificates are marked for EKU with `Code Signing`. They also have
|
||||
/// extensions for `Developer ID Application` and `Developer ID Date`.
|
||||
DeveloperIdApplication,
|
||||
|
||||
/// Developer ID Installer.
|
||||
///
|
||||
/// Certificates are marked for EKU with `Developer ID Application`. They also
|
||||
/// have extensions `Developer ID Installer` and `Developer ID Date`.
|
||||
DeveloperIdInstaller,
|
||||
}
|
||||
|
||||
impl CertificateProfile {
|
||||
pub fn all() -> &'static [Self] {
|
||||
&[
|
||||
Self::MacInstallerDistribution,
|
||||
Self::AppleDistribution,
|
||||
Self::AppleDevelopment,
|
||||
Self::DeveloperIdApplication,
|
||||
Self::DeveloperIdInstaller,
|
||||
]
|
||||
}
|
||||
|
||||
/// Obtain the string values that variants are recognized as.
|
||||
pub fn str_names() -> [&'static str; 5] {
|
||||
[
|
||||
"mac-installer-distribution",
|
||||
"apple-distribution",
|
||||
"apple-development",
|
||||
"developer-id-application",
|
||||
"developer-id-installer",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CertificateProfile {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CertificateProfile::MacInstallerDistribution => f.write_str("mac-installer-distribution"),
|
||||
CertificateProfile::AppleDistribution => f.write_str("apple-distribution"),
|
||||
CertificateProfile::AppleDevelopment => f.write_str("apple-development"),
|
||||
CertificateProfile::DeveloperIdApplication => f.write_str("developer-id-application"),
|
||||
CertificateProfile::DeveloperIdInstaller => f.write_str("developer-id-installer"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CertificateProfile {
|
||||
type Err = AppleCodesignError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s {
|
||||
"apple-distribution" => Ok(Self::AppleDistribution),
|
||||
"apple-development" => Ok(Self::AppleDevelopment),
|
||||
"developer-id-application" => Ok(Self::DeveloperIdApplication),
|
||||
"developer-id-installer" => Ok(Self::DeveloperIdInstaller),
|
||||
"mac-installer-distribution" => Ok(Self::MacInstallerDistribution),
|
||||
_ => Err(AppleCodesignError::UnknownCertificateProfile(s.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extensions to [X509CertificateBuilder] specializing in Apple certificate behavior.
|
||||
///
|
||||
/// Most callers should call [Self::apple_certificate_profile] to configure
|
||||
/// a preset profile for the certificate being generated. After that - and it is
|
||||
/// important it is after - call [Self::apple_subject] to define the subject
|
||||
/// field. If you call this after registering code signing extensions, it
|
||||
/// detects the appropriate format for the Common Name field.
|
||||
pub trait AppleCertificateBuilder: Sized {
|
||||
/// This functions defines common attributes on the certificate subject.
|
||||
///
|
||||
/// `team_id` is your Apple team id. It is a short alphanumeric string. You
|
||||
/// can find this at <https://developer.apple.com/account/#/membership/>.
|
||||
fn apple_subject(
|
||||
&mut self,
|
||||
team_id: &str,
|
||||
person_name: &str,
|
||||
country: &str,
|
||||
) -> Result<(), AppleCodesignError>;
|
||||
|
||||
/// Add an [ExtendedKeyUsagePurpose] to this certificate.
|
||||
fn apple_extended_key_usage(
|
||||
&mut self,
|
||||
usage: ExtendedKeyUsagePurpose,
|
||||
) -> Result<(), AppleCodesignError>;
|
||||
|
||||
/// Add a certificate extension as defined by a [CodeSigningCertificateExtension] instance.
|
||||
fn apple_code_signing_certificate_extension(
|
||||
&mut self,
|
||||
extension: CodeSigningCertificateExtension,
|
||||
) -> Result<(), AppleCodesignError>;
|
||||
|
||||
/// Add a [CertificateProfile] to this builder.
|
||||
///
|
||||
/// All certificate extensions relevant to this profile are added.
|
||||
///
|
||||
/// This should be the first function you call after creating an instance
|
||||
/// because other functions rely on the state that it sets.
|
||||
fn apple_certificate_profile(
|
||||
&mut self,
|
||||
profile: CertificateProfile,
|
||||
) -> Result<(), AppleCodesignError>;
|
||||
|
||||
/// Find code signing extensions that are currently registered.
|
||||
fn apple_code_signing_extensions(&self) -> Vec<CodeSigningCertificateExtension>;
|
||||
}
|
||||
|
||||
impl AppleCertificateBuilder for X509CertificateBuilder {
|
||||
fn apple_subject(
|
||||
&mut self,
|
||||
team_id: &str,
|
||||
person_name: &str,
|
||||
country: &str,
|
||||
) -> Result<(), AppleCodesignError> {
|
||||
// TODO the subject schema here isn't totally accurate. While OU does always
|
||||
// appear to be the team id, the user id attribute can be something else.
|
||||
// For example, for Apple Development, there are a similarly formatted yet
|
||||
// different value. But the team id does still appear.
|
||||
self
|
||||
.subject()
|
||||
.append_utf8_string(Oid(OID_USER_ID.as_ref().into()), team_id)
|
||||
.map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
|
||||
|
||||
// Common Name is derived from the profile in use.
|
||||
|
||||
let extensions = self.apple_code_signing_extensions();
|
||||
|
||||
let common_name = if extensions
|
||||
.contains(&CodeSigningCertificateExtension::DeveloperIdApplication)
|
||||
{
|
||||
format!("Developer ID Application: {person_name} ({team_id})")
|
||||
} else if extensions.contains(&CodeSigningCertificateExtension::DeveloperIdInstaller) {
|
||||
format!("Developer ID Installer: {person_name} ({team_id})")
|
||||
} else if extensions
|
||||
.contains(&CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission)
|
||||
{
|
||||
format!("Apple Distribution: {person_name} ({team_id})")
|
||||
} else if extensions.contains(&CodeSigningCertificateExtension::AppleMacAppSigningSubmission) {
|
||||
format!("3rd Party Mac Developer Installer: {person_name} ({team_id})")
|
||||
} else if extensions.contains(&CodeSigningCertificateExtension::MacDeveloper) {
|
||||
format!("Apple Development: {person_name} ({team_id})")
|
||||
} else {
|
||||
format!("{person_name} ({team_id})")
|
||||
};
|
||||
|
||||
self
|
||||
.subject()
|
||||
.append_common_name_utf8_string(&common_name)
|
||||
.map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
|
||||
|
||||
self
|
||||
.subject()
|
||||
.append_organizational_unit_utf8_string(team_id)
|
||||
.map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
|
||||
|
||||
self
|
||||
.subject()
|
||||
.append_organization_utf8_string(person_name)
|
||||
.map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
|
||||
|
||||
self
|
||||
.subject()
|
||||
.append_printable_string(Oid(OID_COUNTRY_NAME.as_ref().into()), country)
|
||||
.map_err(|e| AppleCodesignError::CertificateBuildError(format!("{e:?}")))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apple_extended_key_usage(
|
||||
&mut self,
|
||||
usage: ExtendedKeyUsagePurpose,
|
||||
) -> Result<(), AppleCodesignError> {
|
||||
let payload =
|
||||
bcder::encode::sequence(Oid(Bytes::copy_from_slice(usage.as_oid().as_ref())).encode())
|
||||
.to_captured(bcder::Mode::Der);
|
||||
|
||||
self.add_extension_der_data(
|
||||
Oid(OID_EXTENDED_KEY_USAGE.as_ref().into()),
|
||||
true,
|
||||
payload.as_slice(),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apple_code_signing_certificate_extension(
|
||||
&mut self,
|
||||
extension: CodeSigningCertificateExtension,
|
||||
) -> Result<(), AppleCodesignError> {
|
||||
let (critical, payload) = match extension {
|
||||
CodeSigningCertificateExtension::IPhoneDeveloper => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.2
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.4
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningDevelopment => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.7
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningSubmission => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.8
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::MacDeveloper => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.12
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::DeveloperIdApplication => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.13
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
CodeSigningCertificateExtension::DeveloperIdInstaller => {
|
||||
// SEQUENCE (3 elem)
|
||||
// OBJECT IDENTIFIER 1.2.840.113635.100.6.1.14
|
||||
// BOOLEAN true
|
||||
// OCTET STRING (2 byte) 0500
|
||||
// NULL
|
||||
(true, Bytes::copy_from_slice(&[0x05, 0x00]))
|
||||
}
|
||||
|
||||
// The rest of these probably have the same payload. But until we see
|
||||
// them, don't take chances.
|
||||
_ => {
|
||||
return Err(AppleCodesignError::CertificateBuildError(format!(
|
||||
"don't know how to handle code signing extension {extension:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
self.add_extension_der_data(
|
||||
Oid(Bytes::copy_from_slice(extension.as_oid().as_ref())),
|
||||
critical,
|
||||
payload,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apple_certificate_profile(
|
||||
&mut self,
|
||||
profile: CertificateProfile,
|
||||
) -> Result<(), AppleCodesignError> {
|
||||
// Try to keep this logic in sync with apple_guess_profile().
|
||||
match profile {
|
||||
CertificateProfile::DeveloperIdApplication => {
|
||||
self.constraint_not_ca();
|
||||
self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?;
|
||||
self.key_usage(KeyUsage::DigitalSignature);
|
||||
|
||||
// OID_EXTENSION_DEVELOPER_ID_DATE comes next. But we don't know what
|
||||
// that should be. It is a UTF8String instead of an ASN.1 time type
|
||||
// because who knows.
|
||||
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::DeveloperIdApplication,
|
||||
)?;
|
||||
}
|
||||
CertificateProfile::DeveloperIdInstaller => {
|
||||
self.constraint_not_ca();
|
||||
self.apple_extended_key_usage(ExtendedKeyUsagePurpose::DeveloperIdInstaller)?;
|
||||
self.key_usage(KeyUsage::DigitalSignature);
|
||||
|
||||
// OID_EXTENSION_DEVELOPER_ID_DATE comes next.
|
||||
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::DeveloperIdInstaller,
|
||||
)?;
|
||||
}
|
||||
CertificateProfile::AppleDevelopment => {
|
||||
self.constraint_not_ca();
|
||||
self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?;
|
||||
self.key_usage(KeyUsage::DigitalSignature);
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::IPhoneDeveloper,
|
||||
)?;
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::MacDeveloper,
|
||||
)?;
|
||||
}
|
||||
CertificateProfile::AppleDistribution => {
|
||||
self.constraint_not_ca();
|
||||
self.apple_extended_key_usage(ExtendedKeyUsagePurpose::CodeSigning)?;
|
||||
self.key_usage(KeyUsage::DigitalSignature);
|
||||
|
||||
// OID_EXTENSION_DEVELOPER_ID_DATE comes next.
|
||||
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningDevelopment,
|
||||
)?;
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::AppleDeveloperCertificateSubmission,
|
||||
)?;
|
||||
}
|
||||
CertificateProfile::MacInstallerDistribution => {
|
||||
self.constraint_not_ca();
|
||||
self.apple_extended_key_usage(ExtendedKeyUsagePurpose::ThirdPartyMacDeveloperInstaller)?;
|
||||
self.key_usage(KeyUsage::DigitalSignature);
|
||||
|
||||
self.apple_code_signing_certificate_extension(
|
||||
CodeSigningCertificateExtension::AppleMacAppSigningSubmission,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apple_code_signing_extensions(&self) -> Vec<CodeSigningCertificateExtension> {
|
||||
self
|
||||
.extensions()
|
||||
.iter()
|
||||
.filter_map(|ext| CodeSigningCertificateExtension::try_from(&ext.id).ok())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new self-signed X.509 certificate suitable for signing code.
|
||||
///
|
||||
/// The created certificate contains all the extensions needed to convey
|
||||
/// that it is used for code signing and should resemble certificates.
|
||||
///
|
||||
/// However, because the certificate isn't signed by Apple or another
|
||||
/// trusted certificate authority, binaries signed with the certificate
|
||||
/// may not pass Apple's verification requirements and the OS may refuse
|
||||
/// to proceed. Needless to say, only use certificates generated with this
|
||||
/// function for testing purposes only.
|
||||
pub fn create_self_signed_code_signing_certificate(
|
||||
algorithm: KeyAlgorithm,
|
||||
profile: CertificateProfile,
|
||||
team_id: &str,
|
||||
person_name: &str,
|
||||
country: &str,
|
||||
validity_duration: chrono::Duration,
|
||||
) -> Result<(CapturedX509Certificate, InMemorySigningKeyPair), AppleCodesignError> {
|
||||
let mut builder = X509CertificateBuilder::default();
|
||||
|
||||
builder.apple_certificate_profile(profile)?;
|
||||
builder.apple_subject(team_id, person_name, country)?;
|
||||
builder.validity_duration(validity_duration);
|
||||
|
||||
// x509-certificate crate doesn't support RSA key generation. So do
|
||||
// that ourselves.
|
||||
if matches!(algorithm, KeyAlgorithm::Rsa) {
|
||||
let private_key = rsa::RsaPrivateKey::new(&mut rand08::thread_rng(), 2048).map_err(|e| {
|
||||
AppleCodesignError::CertificateBuildError(format!("error generating RSA key: {}", e))
|
||||
})?;
|
||||
let key_pair = InMemorySigningKeyPair::from_pkcs8_der(
|
||||
private_key
|
||||
.to_pkcs8_der()
|
||||
.map_err(|e| {
|
||||
AppleCodesignError::CertificateGeneric(format!("error converting RSA key to DER: {}", e))
|
||||
})?
|
||||
.as_bytes(),
|
||||
)?;
|
||||
|
||||
let cert = builder.create_with_key_pair(&key_pair)?;
|
||||
|
||||
Ok((cert, key_pair))
|
||||
} else {
|
||||
Ok(builder.create_with_random_keypair(algorithm)?)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
//
|
||||
// Vendored from apple-codesign 0.29.0 <https://github.com/indygreg/apple-platform-rs>,
|
||||
// Copyright Gregory Szorc and contributors. Modified by the Tauri Programme
|
||||
// within The Commons Conservancy. See README.md in this directory.
|
||||
|
||||
use thiserror::Error;
|
||||
use x509_certificate::X509CertificateError;
|
||||
|
||||
/// Unified error type for Apple code signing.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppleCodesignError {
|
||||
#[error("X.509 certificate handler error: {0}")]
|
||||
X509(#[from] X509CertificateError),
|
||||
|
||||
#[error("certificate error: {0}")]
|
||||
CertificateGeneric(String),
|
||||
|
||||
#[error("the given OID does not match a recognized Apple certificate authority extension")]
|
||||
OidIsntCertificateAuthority,
|
||||
|
||||
#[error("the given OID does not match a recognized Apple code signing extension")]
|
||||
OidIsntCodeSigningExtension,
|
||||
|
||||
#[error("error building certificate: {0}")]
|
||||
CertificateBuildError(String),
|
||||
|
||||
#[error("unknown certificate profile: {0}")]
|
||||
UnknownCertificateProfile(String),
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
mod certificate;
|
||||
mod error;
|
||||
|
||||
pub use certificate::{CertificateProfile, create_self_signed_code_signing_certificate};
|
||||
pub use error::AppleCodesignError;
|
||||
@@ -2,10 +2,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use apple_codesign::create_self_signed_code_signing_certificate;
|
||||
use crate::apple_codesign::create_self_signed_code_signing_certificate;
|
||||
use x509_certificate::{EcdsaCurve, KeyAlgorithm};
|
||||
|
||||
pub use apple_codesign::CertificateProfile;
|
||||
pub use crate::apple_codesign::CertificateProfile;
|
||||
|
||||
use crate::{Error, Result};
|
||||
|
||||
|
||||
@@ -102,29 +102,7 @@ impl Keychain {
|
||||
error,
|
||||
})?;
|
||||
|
||||
assert_command(
|
||||
Command::new("security")
|
||||
.arg("import")
|
||||
.arg(cert_path)
|
||||
.arg("-P")
|
||||
.arg(certificate_password)
|
||||
.args([
|
||||
"-T",
|
||||
"/usr/bin/codesign",
|
||||
"-T",
|
||||
"/usr/bin/pkgbuild",
|
||||
"-T",
|
||||
"/usr/bin/productbuild",
|
||||
])
|
||||
.arg("-k")
|
||||
.arg(&keychain_path)
|
||||
.piped(),
|
||||
"failed to import keychain certificate",
|
||||
)
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "security import".to_string(),
|
||||
error,
|
||||
})?;
|
||||
import_certificate(cert_path, certificate_password, &keychain_path)?;
|
||||
|
||||
assert_command(
|
||||
Command::new("security")
|
||||
@@ -252,3 +230,172 @@ impl Keychain {
|
||||
self.path.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Format of the certificate file handed to `security import`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CertificateFormat {
|
||||
/// PKCS#12 archive (.p12).
|
||||
Pkcs12,
|
||||
/// Sequence of PEM items, e.g. a private key followed by its certificate.
|
||||
PemSequence,
|
||||
}
|
||||
|
||||
impl CertificateFormat {
|
||||
fn detect(contents: &[u8]) -> Self {
|
||||
if contents.trim_ascii_start().starts_with(b"-----BEGIN ") {
|
||||
Self::PemSequence
|
||||
} else {
|
||||
Self::Pkcs12
|
||||
}
|
||||
}
|
||||
|
||||
fn security_format(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pkcs12 => "pkcs12",
|
||||
Self::PemSequence => "pemseq",
|
||||
}
|
||||
}
|
||||
|
||||
fn description(self) -> &'static str {
|
||||
match self {
|
||||
Self::Pkcs12 => "a PKCS#12 (.p12) file",
|
||||
Self::PemSequence => "a PEM bundle",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Imports the certificate and its private key into the given keychain.
|
||||
///
|
||||
/// The format is passed explicitly because `security` fails to infer it for
|
||||
/// p12 files using PBES2/AES encryption (the OpenSSL 3 default).
|
||||
fn import_certificate(
|
||||
cert_path: &Path,
|
||||
certificate_password: &OsString,
|
||||
keychain_path: &Path,
|
||||
) -> Result<()> {
|
||||
let contents = std::fs::read(cert_path).map_err(|error| Error::Fs {
|
||||
context: "failed to read certificate file",
|
||||
path: cert_path.to_path_buf(),
|
||||
error,
|
||||
})?;
|
||||
let format = CertificateFormat::detect(&contents);
|
||||
|
||||
assert_command(
|
||||
Command::new("security")
|
||||
.arg("import")
|
||||
.arg(cert_path)
|
||||
.args(["-f", format.security_format(), "-t", "agg"])
|
||||
.arg("-P")
|
||||
.arg(certificate_password)
|
||||
.args([
|
||||
"-T",
|
||||
"/usr/bin/codesign",
|
||||
"-T",
|
||||
"/usr/bin/pkgbuild",
|
||||
"-T",
|
||||
"/usr/bin/productbuild",
|
||||
])
|
||||
.arg("-k")
|
||||
.arg(keychain_path)
|
||||
.piped(),
|
||||
&format!(
|
||||
"failed to import the signing certificate as {}; the certificate must be a PKCS#12 (.p12) file or a PEM bundle containing both the certificate and its private key, and the password must match the one used to export it",
|
||||
format.description()
|
||||
),
|
||||
)
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: format!("security import -f {}", format.security_format()),
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn detects_certificate_format() {
|
||||
assert_eq!(
|
||||
CertificateFormat::detect(include_bytes!("../tests/fixtures/pbes2.p12")),
|
||||
CertificateFormat::Pkcs12
|
||||
);
|
||||
assert_eq!(
|
||||
CertificateFormat::detect(include_bytes!("../tests/fixtures/bundle.pem")),
|
||||
CertificateFormat::PemSequence
|
||||
);
|
||||
assert_eq!(
|
||||
CertificateFormat::detect(b"\n -----BEGIN CERTIFICATE-----\n"),
|
||||
CertificateFormat::PemSequence
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod import {
|
||||
use super::super::*;
|
||||
|
||||
const PASSWORD: &str = "tauri";
|
||||
|
||||
/// Temporary keychain that is not added to the user's search list.
|
||||
struct TempKeychain {
|
||||
_dir: tempfile::TempDir,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempKeychain {
|
||||
fn new() -> Self {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("test.keychain-db");
|
||||
let status = Command::new("security")
|
||||
.args(["create-keychain", "-p", "test"])
|
||||
.arg(&path)
|
||||
.status()
|
||||
.unwrap();
|
||||
assert!(status.success(), "failed to create test keychain");
|
||||
Self { _dir: dir, path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempKeychain {
|
||||
fn drop(&mut self) {
|
||||
let _ = Command::new("security")
|
||||
.arg("delete-keychain")
|
||||
.arg(&self.path)
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
||||
fn fixture(name: &str) -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
fn import(name: &str, password: &str) -> Result<()> {
|
||||
let keychain = TempKeychain::new();
|
||||
import_certificate(&fixture(name), &password.into(), &keychain.path)
|
||||
}
|
||||
|
||||
// regression test for https://github.com/tauri-apps/tauri/issues/15843
|
||||
#[test]
|
||||
fn imports_pbes2_p12() {
|
||||
import("pbes2.p12", PASSWORD).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_legacy_p12() {
|
||||
import("legacy.p12", PASSWORD).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_pem_bundle() {
|
||||
import("bundle.pem", PASSWORD).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_password_error_explains_requirements() {
|
||||
let error = import("pbes2.p12", "wrong").unwrap_err().to_string();
|
||||
assert!(error.contains("security import -f pkcs12"), "{error}");
|
||||
assert!(error.contains("password must match"), "{error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
mod apple_codesign;
|
||||
pub mod certificate;
|
||||
mod keychain;
|
||||
mod provisioning_profile;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCcHXnG488IHvLs
|
||||
UMI1gowpm0Z9WNh0L5+NJVrw7XaYahcSZZximBU01JjrZWlpZZUe8cgwxE6DfLkp
|
||||
c6cUyXUIWigliHRWHJThMlEhNz66pnp2rBmW+WpiY5mEqg+0osZvsAaAUow1KBOi
|
||||
djb5ZN76Csrb7NQMSlTuPrYqmZUGqy3UdVwSJHpGLrGDTuFw/DOIUB4Xz340Blst
|
||||
Z33vGhX8dk3yFS0TU/rfM0o3cXqzSUV2DEaeQPoP3hRnxVU8JZbumcGjbZ/+lE8H
|
||||
U8+N6kFALClTo+n493FMa+/o3EhuiFKLn9a+hfJ25d36YZtAF9gnCBlfqUAspLYO
|
||||
OXvh4HMlAgMBAAECggEAAlPxMS1L9MXLXwk8QCKEdwlc9XkJ7z+XjDoCn2WXF2I7
|
||||
mldgC9tTM6C48Aeo2Py+CsJVtx5yQkUtmKfkFawEXf3EK/0bKP6PnMUMjyd8/LkH
|
||||
SfY6gnURKEruattDVTD5LwirrCWu+ArzMgC7xJ/DsGxRRpLDPbStDAls23lDTuBg
|
||||
GJk5dHNDcF2zkuA+urGTuCdkmjBl2N7rD16giFCCqlUX4BCKnjuMQl5QHbWlj31O
|
||||
h7qR5sfUowL3QyF1ZmFuKs9/oNVl+VG/VEA1l2MCNlyyMZIyuLNoF9iAs6xyNU9+
|
||||
CoicKCrKeiNBkeXMshKsnr4wFQUah7iK7ZbZ0ABbeQKBgQDZgc+kSsn/b9MNVrjZ
|
||||
OK8HTJCbYrg8oCMzFDVTqEj1hLeE8KVLZKqVrm7Zjg/3RN2imTV+z9xTc5YW1XfG
|
||||
aB/P0vvBPOTzl39OUP8MAbV8/A+iRKl+F6cfJtgkYbHfMnVrPZ+vKLl/MpKBCWtK
|
||||
9YlcmWxGqYpVY9CxJrOrthJjKQKBgQC3vktT7SY/eDZT7cNjmCn2RlP5fAySYyio
|
||||
13XoRzLX0K2jTrynQkxFUZ/V1m9C0pbat1pABPNwwO29PDD/DfXJI/TbcVUu7rgZ
|
||||
ZwNFMUeq0wjM7oYI4slVrsDntTtWokBC6xI+07NDrKfKZmtzXJmyQke246cwP+9r
|
||||
n5tzWFvrnQKBgQCYO2fF+GZqtqyDwHe96RNvy2HGr5B9NiOMw48TT0xJMqtziy5V
|
||||
l05hVuXUtL5t9TAedpbfe8IcUmDw+FVNc2EYhvWXaPRhnV4HxV7M24FXyd9zVhU+
|
||||
4HUis4BB7nKc4ecWuNnDsYArC41IbPxASZvrSKJkzpK4cxUEeC72dodmCQKBgGw/
|
||||
Fj+WOW8w0ci2DOLSlP36RqR2uS1Qb3awoEy8DS7pSDIqQ6fBvg8kXCZtEtTqHWcy
|
||||
l1+ZyxUFA8n0+OR5g/AAV9MjvExO5HRKpLmRo6f2dXvNlXcMrBHnRsv6j7+lDHif
|
||||
5frPWw+mWp85oVmwFvfWZktPaIJbWCAuQjU8u8JdAoGAPEGLcKDugDai0VPS1ATz
|
||||
idYEGinjwJBOoO+ihvULAirGjJbV+C9yCE2csObxMIyiAAhNorEROHWqzBhPN/E8
|
||||
rEoZ8NWYeO4amrI77f14m89f05AzYa52fQQzZxZmNhOSJMDZRHLgNRAqh3aescvx
|
||||
QiWEtDrshjoGwd9Q2nNfIek=
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDNzCCAh+gAwIBAgIUIcKIESrfC7Zko+mWQxGlUzof/7AwDQYJKoZIhvcNAQEL
|
||||
BQAwMTEbMBkGA1UEAwwSVGF1cmkgVGVzdCBTaWduaW5nMRIwEAYDVQQLDAlUQVVS
|
||||
SVRFU1QwHhcNMjYxMDE0MTM1NDU2WhcNMzYxMDExMTM1NDU2WjAxMRswGQYDVQQD
|
||||
DBJUYXVyaSBUZXN0IFNpZ25pbmcxEjAQBgNVBAsMCVRBVVJJVEVTVDCCASIwDQYJ
|
||||
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJwdecbjzwge8uxQwjWCjCmbRn1Y2HQv
|
||||
n40lWvDtdphqFxJlnGKYFTTUmOtlaWlllR7xyDDEToN8uSlzpxTJdQhaKCWIdFYc
|
||||
lOEyUSE3PrqmenasGZb5amJjmYSqD7Sixm+wBoBSjDUoE6J2Nvlk3voKytvs1AxK
|
||||
VO4+tiqZlQarLdR1XBIkekYusYNO4XD8M4hQHhfPfjQGWy1nfe8aFfx2TfIVLRNT
|
||||
+t8zSjdxerNJRXYMRp5A+g/eFGfFVTwllu6ZwaNtn/6UTwdTz43qQUAsKVOj6fj3
|
||||
cUxr7+jcSG6IUouf1r6F8nbl3fphm0AX2CcIGV+pQCyktg45e+HgcyUCAwEAAaNH
|
||||
MEUwHQYDVR0OBBYEFNNj9xD+UtUdmi0N+cLKt3uJuye1MA8GA1UdEwEB/wQFMAMB
|
||||
Af8wEwYDVR0lBAwwCgYIKwYBBQUHAwMwDQYJKoZIhvcNAQELBQADggEBAFjawc2z
|
||||
6frWIWiTRJ+wlP3VmQkZsxmkHhs9RfBPeqgy+GNxBdgFdXxkW02evuiz4409C6PT
|
||||
r8eNpAtcdks4+pfTFULTi1jnsJns0neypesvk8BKyVSgmSkhhlO59Th6E2A5kVvu
|
||||
FyZ3mQXe5dQmvGoOR7De6h5PEHabDrAMuZ81+TnWUq+qwWcaD98BTJCrqzO+Ncn4
|
||||
jl4dKWwaOg2/Afd06RD0mdqmdMM501ToEs+68KS9G+V+vn8Aobe2v8ckxjylixGJ
|
||||
K1LBMaKaOKQ5tNL4c9qjJsW/peYLGRPFevSCU9R3DtNIEVt28kKWs3T84ZwkAq8d
|
||||
fgLL9ac1N/KWbr8=
|
||||
-----END CERTIFICATE-----
|
||||
Binary file not shown.
Binary file not shown.
@@ -54,6 +54,14 @@
|
||||
### Breaking Changes
|
||||
|
||||
- [`1e5ba7b53`](https://www.github.com/tauri-apps/tauri/commit/1e5ba7b53dfb3da97f372b646f15853e5ba0e1a8) ([#15985](https://www.github.com/tauri-apps/tauri/pull/15985)) The `default_runtime` attribute macro was removed, the generic types of `tauri` now default to `tauri::DynRuntime` directly.
|
||||
|
||||
## [2.7.3]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-codegen@2.7.3`
|
||||
|
||||
## [2.7.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -49,6 +49,12 @@
|
||||
|
||||
- Upgraded to `tauri-utils@3.0.0-alpha.0`
|
||||
|
||||
## [2.7.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
|
||||
## [2.7.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -99,6 +99,13 @@
|
||||
- [`1e5ba7b53`](https://www.github.com/tauri-apps/tauri/commit/1e5ba7b53dfb3da97f372b646f15853e5ba0e1a8) ([#15985](https://www.github.com/tauri-apps/tauri/pull/15985)) The `Wry<T>` runtime type was renamed to `WryRuntime<T>` (defaulting to `tauri::EventLoopMessage`), and `Wry` is now the unit-like attributes type that selects the runtime, e.g. `tauri::Builder::default().runtime(tauri_runtime_wry::Wry::default())`. `WryHandle::plugin` now takes `&self`.
|
||||
- [`1e5ba7b53`](https://www.github.com/tauri-apps/tauri/commit/1e5ba7b53dfb3da97f372b646f15853e5ba0e1a8) ([#15985](https://www.github.com/tauri-apps/tauri/pull/15985)) The `WebviewAttribute` enum was replaced by the `WryWebviewAttributes` struct, with the `environment` (Windows), `related_view` (Linux) and `webview_configuration` (macOS) fields. The `AsWryWebviewAttributes` trait gives the `WebviewWindowBuilderWryExt` and `WebviewBuilderWryExt` extension traits access to it on both `WryRuntime` and `tauri::DynRuntime`.
|
||||
|
||||
## [2.12.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-runtime@2.12.2`
|
||||
|
||||
## [2.12.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -79,6 +79,12 @@
|
||||
- Runtime crates must implement `From<Self>` for `dynamic::DynRuntimeInitAttrs` so their attributes can be passed to the type-erased builder.
|
||||
- `Runtime::WindowOpener` and `window::WindowBuilderBase` now require `'static`, so they can be type-erased.
|
||||
|
||||
## [2.12.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
|
||||
## [2.12.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -50,6 +50,12 @@
|
||||
- [`c9277f3c0`](https://www.github.com/tauri-apps/tauri/commit/c9277f3c0c24518a7ab7d7d1f2489e004b1597f7) Set MSRV to 1.95.
|
||||
- [`19929799f`](https://www.github.com/tauri-apps/tauri/commit/19929799f42398a6e85adb00ae02f2e7fe46d214) First v3 alpha release!
|
||||
|
||||
## [2.10.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`141d1c0d6`](https://www.github.com/tauri-apps/tauri/commit/141d1c0d6f3a746f7f34618d00c2676c902dca67) ([#16199](https://www.github.com/tauri-apps/tauri/pull/16199)) Fix the `failed to read plugin permissions` build error when the Cargo target directory is moved after the first build, for example when it is restored from a CI cache at a different path. Permission files generated inside `OUT_DIR` are now listed relative to it.
|
||||
|
||||
## [2.10.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -104,9 +104,22 @@ pub fn define_permissions_from_files(
|
||||
pkg_name: &str,
|
||||
out_dir: &Path,
|
||||
) -> Result<Vec<PermissionFile>, Error> {
|
||||
// Files inside `out_dir` are listed relative to it so the list survives the target dir being
|
||||
// moved: Cargo rewrites the `OUT_DIR` path it hands to dependents, not the paths in this file.
|
||||
let canonical_out_dir = out_dir.canonicalize().ok();
|
||||
let listed_files = permission_files
|
||||
.iter()
|
||||
.map(|path| {
|
||||
canonical_out_dir
|
||||
.as_deref()
|
||||
.and_then(|dir| path.strip_prefix(dir).ok())
|
||||
.unwrap_or(path)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let pkg_name_valid_path = pkg_name.replace(':', "-");
|
||||
let permission_files_path = out_dir.join(format!("{pkg_name_valid_path}-permission-files"));
|
||||
let permission_files_json = serde_json::to_string(&permission_files)?;
|
||||
let permission_files_json = serde_json::to_string(&listed_files)?;
|
||||
|
||||
write_if_changed(&permission_files_path, permission_files_json)
|
||||
.map_err(|e| Error::WriteFile(e, permission_files_path.clone()))?;
|
||||
@@ -137,6 +150,15 @@ pub fn define_permissions<F: Fn(&Path) -> bool>(
|
||||
define_permissions_from_files(permission_files, pkg_name, out_dir)
|
||||
}
|
||||
|
||||
/// Read the permission files listed in a file written by [`define_permissions`].
|
||||
fn read_permission_files(list_path: &Path) -> Result<Vec<PermissionFile>, Error> {
|
||||
let list = fs::read_to_string(list_path).map_err(|e| Error::ReadFile(e, list_path.into()))?;
|
||||
let paths: Vec<PathBuf> = serde_json::from_str(&list)?;
|
||||
// relative entries point into the out dir the list was written to
|
||||
let out_dir = list_path.parent().unwrap();
|
||||
parse_permissions(paths.into_iter().map(|path| out_dir.join(path)).collect())
|
||||
}
|
||||
|
||||
/// Read all permissions listed from the defined cargo cfg key value.
|
||||
pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error> {
|
||||
let mut permissions_map = HashMap::new();
|
||||
@@ -153,11 +175,7 @@ pub fn read_permissions() -> Result<HashMap<String, Vec<PermissionFile>>, Error>
|
||||
.unwrap_or(v)
|
||||
})
|
||||
{
|
||||
let permissions_path = PathBuf::from(value);
|
||||
let permissions_str =
|
||||
fs::read_to_string(&permissions_path).map_err(|e| Error::ReadFile(e, permissions_path))?;
|
||||
let permissions: Vec<PathBuf> = serde_json::from_str(&permissions_str)?;
|
||||
let permissions = parse_permissions(permissions)?;
|
||||
let permissions = read_permission_files(Path::new(&value))?;
|
||||
|
||||
let plugin_crate_name = plugin_crate_name_var.to_lowercase().replace('_', "-");
|
||||
let plugin_crate_name = plugin_crate_name
|
||||
@@ -557,3 +575,43 @@ pub fn generate_allowed_commands(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn permission_files_resolve_after_out_dir_is_moved() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let out_dir = root.path().join("out");
|
||||
autogenerate_command_permissions(&out_dir.join("permissions"), &["generated"], "", false);
|
||||
autogenerate_command_permissions(&root.path().join("plugin"), &["custom"], "", false);
|
||||
|
||||
let pattern = format!(
|
||||
"{}/**/*.toml",
|
||||
glob::Pattern::escape(&root.path().to_string_lossy())
|
||||
);
|
||||
define_permissions(&pattern, "tauri:test", &out_dir, |_| true).unwrap();
|
||||
|
||||
// e.g. a target dir restored from a CI cache at a different path
|
||||
let moved_out_dir = root.path().join("moved");
|
||||
fs::rename(&out_dir, &moved_out_dir).unwrap();
|
||||
|
||||
let permission_files =
|
||||
read_permission_files(&moved_out_dir.join("tauri-test-permission-files")).unwrap();
|
||||
let identifiers = permission_files
|
||||
.iter()
|
||||
.flat_map(|file| &file.permission)
|
||||
.map(|permission| permission.identifier.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
identifiers,
|
||||
[
|
||||
"allow-generated",
|
||||
"deny-generated",
|
||||
"allow-custom",
|
||||
"deny-custom"
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -138,6 +138,26 @@
|
||||
- Enabling both selects GTK 4, the same precedence `muda` and `tray-icon` use. Cargo does that whenever the dependency graph contains runtime crates that disagree on the GTK version, and such a binary can only ever run one of them, because GTK 3 and GTK 4 cannot be initialized in the same process. The GTK APIs and the Linux menu integration now fail with the new `Error::GtkVersionMismatch` under a runtime whose GTK version is not the one that was selected, instead of reinterpreting its window objects.
|
||||
- The `test` feature no longer implies GTK 3, so it can be combined with `gtk4`.
|
||||
- [`c8c75b1f7`](https://www.github.com/tauri-apps/tauri/commit/c8c75b1f7f43e7cb1e7d773ed2f6f96fad2fe975) ([#15787](https://www.github.com/tauri-apps/tauri/pull/15787)) The Linux tray icon now uses the ksni (StatusNotifierItem over D-Bus) backend by default instead of libappindicator, dropping the libayatana-appindicator system dependency. The `tray-icon` feature no longer needs a GTK version to be selected. Enable the new `linux-libappindicator` feature to go back to the libappindicator backend.
|
||||
|
||||
## [2.12.4]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`5d1e8777a`](https://www.github.com/tauri-apps/tauri/commit/5d1e8777a04dfee9eb4e5ef4eb4664c8914ef79c) ([#16286](https://www.github.com/tauri-apps/tauri/pull/16286)) Improve the error message on iOS when the development server cannot be reached through a `localhost` URL, suggesting `tauri ios dev --host`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`9c3fb8865`](https://www.github.com/tauri-apps/tauri/commit/9c3fb8865fcc33982dff31c821d988a9a038ff8c) ([#16197](https://www.github.com/tauri-apps/tauri/pull/16197)) Fix a panic in the IPC and asset protocol handlers when the request URI has an empty path (for example `ipc:foo`).
|
||||
- [`7b673ddaa`](https://www.github.com/tauri-apps/tauri/commit/7b673ddaaf885ecaecfe79dfeb4cac1c6880fce6) ([#16146](https://www.github.com/tauri-apps/tauri/pull/16146)) `PluginHandle::run_mobile_plugin_async` and `PluginHandle::run_mobile_plugin` no longer panic (which aborts the process on iOS and Android) when a mobile plugin command resolves after its caller stopped waiting, for example when the async call was cancelled by a timeout. The late response is dropped instead.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-build@2.7.3`
|
||||
- Upgraded to `tauri-utils@2.10.2`
|
||||
- Upgraded to `tauri-runtime@2.12.2`
|
||||
- Upgraded to `tauri-runtime-wry@2.12.2`
|
||||
- Upgraded to `tauri-macros@2.7.3`
|
||||
|
||||
## [2.12.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -439,7 +439,8 @@ fn parse_invoke_request<R: Runtime>(
|
||||
let (parts, mut body) = request.into_parts();
|
||||
|
||||
// skip leading `/`
|
||||
let cmd = percent_encoding::percent_decode(&parts.uri.path().as_bytes()[1..])
|
||||
let path = parts.uri.path().as_bytes();
|
||||
let cmd = percent_encoding::percent_decode(path.strip_prefix(b"/").unwrap_or(path))
|
||||
.decode_utf8_lossy()
|
||||
.to_string();
|
||||
|
||||
@@ -640,6 +641,40 @@ mod tests {
|
||||
assert_eq!(invoke_request.body, InvokeBody::Json(body));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_invoke_request_empty_path() {
|
||||
let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
|
||||
let manager: AppManager<Wry> = AppManager::with_handlers(
|
||||
context,
|
||||
PluginStore::default(),
|
||||
Box::new(|_| false),
|
||||
None,
|
||||
#[cfg(any(target_os = "macos", target_os = "ios"))]
|
||||
None,
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
StateManager::new(),
|
||||
Default::default(),
|
||||
#[cfg(all(desktop, feature = "tray-icon"))]
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
"".into(),
|
||||
None,
|
||||
crate::generate_invoke_key().unwrap(),
|
||||
);
|
||||
|
||||
// `ipc:foo` parses as authority-form, so its path is empty;
|
||||
// this used to panic when stripping the leading `/`
|
||||
let uri = http::Uri::from_static("ipc:foo");
|
||||
assert_eq!(uri.path(), "");
|
||||
|
||||
let request = Request::builder().uri(uri).body(Vec::new()).unwrap();
|
||||
|
||||
assert!(super::parse_invoke_request(&manager, request).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "isolation")]
|
||||
fn parse_invoke_request_isolation() {
|
||||
|
||||
@@ -307,7 +307,15 @@ impl<R: Runtime> PluginHandle<R> {
|
||||
command,
|
||||
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
|
||||
move |response| {
|
||||
tx.lock().unwrap().take().unwrap().send(response).unwrap();
|
||||
// The receiver is gone when the caller stopped waiting, for example because
|
||||
// the future was dropped by a timeout, and the sender is gone when the
|
||||
// response arrives twice (the Android `run_command` keeps a clone of the
|
||||
// handler for its error path). This closure runs inside the native response
|
||||
// handler, an `extern "C"` / JNI function where a panic cannot unwind and
|
||||
// aborts the process, so a late or duplicate response is dropped instead.
|
||||
if let Some(tx) = tx.lock().unwrap().take() {
|
||||
let _ = tx.send(response);
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -336,7 +344,8 @@ impl<R: Runtime> PluginHandle<R> {
|
||||
command,
|
||||
serde_json::to_value(payload).map_err(PluginInvokeError::CannotSerializePayload)?,
|
||||
move |response| {
|
||||
tx.send(response).unwrap();
|
||||
// Never panic inside the native response handler, see `run_mobile_plugin_async`.
|
||||
let _ = tx.send(response);
|
||||
},
|
||||
)?;
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ async fn get_response(
|
||||
window_origin: &str,
|
||||
) -> Result<Response<Cow<'static, [u8]>>, Box<dyn std::error::Error>> {
|
||||
// skip leading `/`
|
||||
let path = request.uri().path().as_bytes();
|
||||
let path =
|
||||
percent_encoding::percent_decode(&request.uri().path().as_bytes()[1..]).decode_utf8_lossy();
|
||||
percent_encoding::percent_decode(path.strip_prefix(b"/").unwrap_or(path)).decode_utf8_lossy();
|
||||
|
||||
let mut resp = Response::builder().header("Access-Control-Allow-Origin", window_origin);
|
||||
|
||||
@@ -306,4 +307,24 @@ mod tests {
|
||||
.ends_with(format!("\r\n--{boundary}--\r\n").as_bytes())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_path_request() {
|
||||
let app = crate::test::mock_app();
|
||||
|
||||
let scope = Scope::new(&app, &FsScope::default()).unwrap();
|
||||
|
||||
// `asset:foo` parses as authority-form, so its path is empty;
|
||||
// this used to panic when stripping the leading `/`
|
||||
let uri = http::Uri::from_static("asset:foo");
|
||||
assert_eq!(uri.path(), "");
|
||||
|
||||
let request = Request::builder().uri(uri).body(Vec::new()).unwrap();
|
||||
|
||||
let response =
|
||||
crate::async_runtime::block_on(get_response(request, &scope, "http://tauri.localhost"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
}
|
||||
@@ -228,7 +228,15 @@ async fn proxy_dev_request(
|
||||
if let Some(s) = e.status() {
|
||||
format!("status code: {}", s.as_u16())
|
||||
} else if cfg!(target_os = "ios") {
|
||||
", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string()
|
||||
let is_localhost = url::Url::parse(&url)
|
||||
.ok()
|
||||
.and_then(|u| u.host_str().map(|h| matches!(h, "localhost" | "127.0.0.1" | "[::1]")))
|
||||
.unwrap_or(false);
|
||||
if is_localhost {
|
||||
", the development server URL points to localhost, which on a physical device resolves to the device itself rather than your machine. Run `tauri ios dev --host` to expose the development server on your network address (it must be configured to listen on the `TAURI_DEV_HOST` address). If you are running on a simulator, make sure the development server is running.".to_string()
|
||||
} else {
|
||||
", did you grant local network permissions? That is required to reach the development server. Please grant the permission via the prompt or in `Settings > Privacy & Security > Local Network` and restart the app. See https://support.apple.com/en-us/102229 for more information.".to_string()
|
||||
}
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ normally just dropping in one more spec.
|
||||
its bundle identifier, so the config has the driver match that name too
|
||||
(`appium:additionalWebviewBundleIds`). The driver also starts with a script timeout of
|
||||
0, which the config raises to the 30s the other drivers default to, or every
|
||||
`executeAsync` would time out at once.
|
||||
promise-returning `execute` calls would time out at once.
|
||||
- Specs never `eval` in the page. They pass a function to the [`tauri()`](test/helpers/index.ts)
|
||||
helper, which serializes it and runs it via the driver's own (CSP-exempt) script injection,
|
||||
handing it `window.__TAURI__` as the first argument and returning its JSON result.
|
||||
|
||||
@@ -16,20 +16,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@crabnebula/tauri-driver": "^2.0.9",
|
||||
"@crabnebula/test-runner-backend": "^0.3.0",
|
||||
"@crabnebula/test-runner-backend": "^0.3.1",
|
||||
"@tauri-apps/api": "../api/dist",
|
||||
"@types/node": "^22.10.0",
|
||||
"@wdio/appium-service": "^9.31.9",
|
||||
"@wdio/cli": "^9.31.9",
|
||||
"@wdio/globals": "^9.31.3",
|
||||
"@wdio/local-runner": "^9.31.9",
|
||||
"@wdio/mocha-framework": "^9.31.9",
|
||||
"@wdio/spec-reporter": "^9.31.2",
|
||||
"@wdio/appium-service": "^10.0.0",
|
||||
"@wdio/cli": "^10.0.0",
|
||||
"@wdio/globals": "^10.0.0",
|
||||
"@wdio/local-runner": "^10.0.0",
|
||||
"@wdio/mocha-framework": "^10.0.0",
|
||||
"@wdio/spec-reporter": "^10.0.0",
|
||||
"appium": "^3.7.0",
|
||||
"appium-uiautomator2-driver": "^8.7.0",
|
||||
"appium-xcuitest-driver": "^12.12.4",
|
||||
"cross-env": "^10.1.0",
|
||||
"typescript": "^6.0.3",
|
||||
"webdriverio": "^9.31.9"
|
||||
"webdriverio": "^10.0.0"
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,11 @@ type PageOutcome<T> =
|
||||
| { ok: true; value: T }
|
||||
| { ok: false; error: string; stack?: string }
|
||||
|
||||
const AsyncFunction = Object.getPrototypeOf(async function () {})
|
||||
.constructor as {
|
||||
new (body: string): (...args: unknown[]) => Promise<string>
|
||||
}
|
||||
|
||||
/** Thrown when the function passed to {@link tauri} rejects inside the webview. */
|
||||
export class TauriPageError extends Error {
|
||||
pageStack?: string
|
||||
@@ -53,41 +58,39 @@ export async function tauri<R, A extends unknown[]>(
|
||||
fn: (api: Api, ...args: A) => R,
|
||||
...args: A
|
||||
): Promise<Awaited<R>> {
|
||||
// WebdriverIO 10 selects Classic's async script endpoint only for an actual
|
||||
// AsyncFunction.
|
||||
// A string body (rather than passing `fn` directly) keeps this working across
|
||||
// both the classic and bidi WebDriver protocols and avoids any in-page eval of
|
||||
// our own — the driver injects this script itself, which is exempt from the
|
||||
// app's CSP. `executeAsync` is used because promise support in `execute` is not
|
||||
// uniform across the platform drivers tauri-driver proxies to.
|
||||
// app's CSP.
|
||||
//
|
||||
// The outcome crosses the driver as a JSON string rather than an object so no
|
||||
// driver gets to interpret its shape: the Selenium atoms that Appium runs
|
||||
// scripts through on iOS turn any object with a numeric `length` property
|
||||
// into an array.
|
||||
const script = `
|
||||
var done = arguments[arguments.length - 1];
|
||||
var args = Array.prototype.slice.call(arguments, 0, arguments.length - 1);
|
||||
var fn = (${fn.toString()});
|
||||
Promise.resolve()
|
||||
.then(function () { return fn.apply(null, [window.__TAURI__].concat(args)); })
|
||||
.then(
|
||||
function (value) { return { ok: true, value: value === undefined ? null : value }; },
|
||||
function (error) {
|
||||
return {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
};
|
||||
}
|
||||
)
|
||||
.then(function (outcome) {
|
||||
try {
|
||||
done(JSON.stringify(outcome));
|
||||
} catch (error) {
|
||||
done(JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error }));
|
||||
}
|
||||
});
|
||||
`
|
||||
const raw = await browser.executeAsync(script, ...args)
|
||||
const script = new AsyncFunction(`
|
||||
const fn = (${fn.toString()});
|
||||
const args = Array.prototype.slice.call(arguments);
|
||||
|
||||
let outcome
|
||||
try {
|
||||
const value = await fn.apply(null, [window.__TAURI__].concat(args));
|
||||
outcome = { ok: true, value: value === undefined ? null : value };
|
||||
} catch (error) {
|
||||
outcome = {
|
||||
ok: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined
|
||||
};
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(outcome);
|
||||
} catch (error) {
|
||||
return JSON.stringify({ ok: false, error: 'result is not JSON-serializable: ' + error });
|
||||
}
|
||||
`)
|
||||
const raw = await browser.execute(script, ...args)
|
||||
const outcome = (
|
||||
typeof raw === 'string' ? JSON.parse(raw) : raw
|
||||
) as PageOutcome<Awaited<R>> | null
|
||||
@@ -118,7 +121,7 @@ export async function tauriError<A extends unknown[]>(
|
||||
}
|
||||
// Some platform drivers (notably the Linux WebKitWebDriver) surface a
|
||||
// page-side `invoke` rejection as a WebDriver-level error on the
|
||||
// `execute/async` command instead of letting the in-page bridge report it
|
||||
// script execution command instead of letting the in-page bridge report it
|
||||
// as an `{ ok: false }` outcome. Fall back to that error's message so the
|
||||
// backend rejection is still assertable. This is safe for error-path specs:
|
||||
// they match the message against an expected pattern, so a genuine driver
|
||||
|
||||
@@ -232,8 +232,8 @@ export const config: WebdriverIO.Config = {
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
try {
|
||||
return (await browser.executeAsync(
|
||||
'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");'
|
||||
return (await browser.execute(
|
||||
'return typeof window.__TAURI__ !== "undefined";'
|
||||
)) as boolean
|
||||
} catch {
|
||||
// A command issued mid-navigation can fail on a stale execution
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
// Appium: the UiAutomator2 driver (Android; chromedriver attaches to the
|
||||
// WebView) or the XCUITest driver (iOS simulator; WebKit remote inspector).
|
||||
// Debug builds enable webview debugging on both platforms, which is what makes
|
||||
// the `WEBVIEW_*` context — and `browser.executeAsync` inside it — available.
|
||||
// the `WEBVIEW_*` context — and `browser.execute` inside it — available.
|
||||
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
@@ -210,8 +210,8 @@ export function mobileConfig(platform: MobilePlatform): WebdriverIO.Config {
|
||||
await browser.waitUntil(
|
||||
async () => {
|
||||
try {
|
||||
return (await browser.executeAsync(
|
||||
'var done = arguments[arguments.length - 1]; done(typeof window.__TAURI__ !== "undefined");'
|
||||
return (await browser.execute(
|
||||
'return typeof window.__TAURI__ !== "undefined";'
|
||||
)) as boolean
|
||||
} catch {
|
||||
// A command issued mid-navigation can fail on a stale execution
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
### What's Changed
|
||||
|
||||
- [`19929799f`](https://www.github.com/tauri-apps/tauri/commit/19929799f42398a6e85adb00ae02f2e7fe46d214) First v3 alpha release!
|
||||
|
||||
## [2.12.4]
|
||||
|
||||
### What's Changed
|
||||
|
||||
- [`dc637db65`](https://www.github.com/tauri-apps/tauri/commit/dc637db6573443cc668b06deed1f014c4ac01033) ([#16260](https://www.github.com/tauri-apps/tauri/pull/16260)) Improve menu documentation.
|
||||
|
||||
## [2.12.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -56,18 +56,18 @@ export class Menu extends MenuItemBase {
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu, Submenu } from '@tauri-apps/api/menu';
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.new({
|
||||
* items: [
|
||||
* await Submenu.new({
|
||||
* {
|
||||
* text: 'File',
|
||||
* items: [
|
||||
* { id: 'open', text: 'Open', accelerator: 'CmdOrCtrl+O', action: () => console.log('open') },
|
||||
* { item: 'Separator' },
|
||||
* { item: 'Quit' }
|
||||
* ]
|
||||
* })
|
||||
* }
|
||||
* ]
|
||||
* });
|
||||
* ```
|
||||
@@ -94,10 +94,10 @@ export class Menu extends MenuItemBase {
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu, Submenu } from '@tauri-apps/api/menu';
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* await menu.append(await Submenu.new({ text: 'Tools', items: [{ id: 'fmt', text: 'Format' }] }));
|
||||
* await using menu = await Menu.default();
|
||||
* await menu.append({ text: 'Tools', items: [{ id: 'fmt', text: 'Format' }] });
|
||||
* await menu.setAsAppMenu();
|
||||
* ```
|
||||
*/
|
||||
@@ -301,7 +301,7 @@ export class Menu extends MenuItemBase {
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* await using menu = await Menu.default();
|
||||
* const previous = await menu.setAsAppMenu();
|
||||
* await previous?.close();
|
||||
* ```
|
||||
@@ -327,7 +327,7 @@ export class Menu extends MenuItemBase {
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
*
|
||||
* const menu = await Menu.default();
|
||||
* await using menu = await Menu.default();
|
||||
* await menu.setAsWindowMenu(getCurrentWindow());
|
||||
* ```
|
||||
*
|
||||
|
||||
@@ -123,7 +123,7 @@ export interface PredefinedMenuItemOptions {
|
||||
* ```typescript
|
||||
* import { Menu, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu';
|
||||
*
|
||||
* const edit = await Submenu.new({
|
||||
* await using edit = await Submenu.new({
|
||||
* text: 'Edit',
|
||||
* items: [
|
||||
* { item: 'Undo' },
|
||||
@@ -136,7 +136,7 @@ export interface PredefinedMenuItemOptions {
|
||||
* ]
|
||||
* });
|
||||
*
|
||||
* const quit = await PredefinedMenuItem.new({ item: 'Quit', text: 'Quit my app' });
|
||||
* await using quit = await PredefinedMenuItem.new({ item: 'Quit', text: 'Quit my app' });
|
||||
* const menu = await Menu.new({ items: [edit, quit] });
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -167,10 +167,10 @@ export interface TrayIconOptions {
|
||||
*
|
||||
* #### Warning
|
||||
*
|
||||
* Unlike Rust, javascript does not have any way to run cleanup code
|
||||
* Unlike Rust, JavaScript does not have any way to run cleanup code
|
||||
* when an object is being removed by garbage collection, but this tray icon
|
||||
* will be cleaned up when the tauri app exists, however if you want to cleanup
|
||||
* this object early, you need to call {@linkcode TrayIcon.close}.
|
||||
* and remove the tray early, you need to call {@linkcode TrayIcon.close}.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
@@ -239,7 +239,7 @@ export class TrayIcon extends Resource {
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* import { defaultWindowIcon } from '@tauri-apps/api/app';
|
||||
*
|
||||
* const menu = await Menu.new({
|
||||
* await using menu = await Menu.new({
|
||||
* items: [{ id: 'quit', text: 'Quit', action: () => console.log('quit') }]
|
||||
* });
|
||||
*
|
||||
@@ -314,7 +314,7 @@ export class TrayIcon extends Resource {
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Menu } from '@tauri-apps/api/menu';
|
||||
* const menu = await Menu.new({ items: [{ id: 'quit', text: 'Quit' }] });
|
||||
* await using menu = await Menu.new({ items: [{ id: 'quit', text: 'Quit' }] });
|
||||
* await tray.setMenu(menu);
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -79,6 +79,19 @@
|
||||
- The CEF files, code signing entitlements and macOS dev flow are only used when the app uses CEF.
|
||||
- Nothing runtime-specific is done for other runtimes.
|
||||
- The app and plugin templates add the `tauri-runtime-wry` dependency and select it with `tauri::Builder::default().runtime(tauri_runtime_wry::Wry::default())`.
|
||||
|
||||
## [2.12.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`c95be4615`](https://www.github.com/tauri-apps/tauri/commit/c95be4615a432b1d12409d3684c960e8aa718ada) ([#16287](https://www.github.com/tauri-apps/tauri/pull/16287)) Fix `tauri ios xcode-script` failing to find the Tauri project when Xcode inherits package manager environment variables from `tauri ios dev --open`, or when pnpm runs the CLI directly from the `gen/apple` folder.
|
||||
- [`d62e5d9c0`](https://www.github.com/tauri-apps/tauri/commit/d62e5d9c0878b6fb327543cc9be50aa1696ee23c) ([#16184](https://www.github.com/tauri-apps/tauri/pull/16184)) Fix NSIS installer leaving `uninstall.exe` and the installation directory behind when uninstalling an existing installation from the installer.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-cli@2.12.3`
|
||||
- [`5a2953e55`](https://www.github.com/tauri-apps/tauri/commit/5a2953e555b4444bed68bd5324ea13ad85a6c06e) ([#16237](https://www.github.com/tauri-apps/tauri/pull/16237)) Updated NSIS to 3.13
|
||||
|
||||
## [2.12.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
Generated
+611
-1051
File diff suppressed because it is too large.
Load diff
+7
-6
@@ -23,7 +23,6 @@ allowBuilds:
|
||||
|
||||
overrides:
|
||||
axios@<1.20.0: ^1.20.0
|
||||
basic-ftp@<=6.2.0: ^6.2.1
|
||||
morgan@<1.12.0: ^1.12.0
|
||||
serialize-javascript@<7.0.5: ^7.0.5
|
||||
sharp@<0.35.5: ^0.35.5
|
||||
@@ -32,10 +31,12 @@ overrides:
|
||||
|
||||
auditConfig:
|
||||
ignoreGhsas:
|
||||
# extract-zip symlink path traversal; no patched release exists (latest is
|
||||
# still 2.0.1). Reached via @wdio/utils > @puppeteer/browsers, which only
|
||||
# unpacks the browser drivers we never download (see allowBuilds above)
|
||||
- GHSA-jmr9-qjv8-65gv
|
||||
- GHSA-7pqw-9j4j-h8q3
|
||||
# braces, no fix available yet
|
||||
- GHSA-vfj7-8cjw-p6xm
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
- '@crabnebula/test-runner-backend-darwin-arm64@0.3.1'
|
||||
- '@crabnebula/test-runner-backend-darwin-x64@0.3.1'
|
||||
- '@crabnebula/test-runner-backend-linux-x64-gnu@0.3.1'
|
||||
- '@crabnebula/test-runner-backend-win32-x64-msvc@0.3.1'
|
||||
- '@crabnebula/test-runner-backend@0.3.1'
|
||||
+2
-2
@@ -21,8 +21,8 @@ fundedBy:
|
||||
- name: CrabNebula
|
||||
uri: 'https://crabnebula.dev'
|
||||
|
||||
softwareVersion: '2.12.3'
|
||||
releaseDate: '2026-10-10'
|
||||
softwareVersion: '2.12.4'
|
||||
releaseDate: '2026-10-14'
|
||||
|
||||
platforms:
|
||||
- windows
|
||||
|
||||
@@ -35,6 +35,11 @@
|
||||
"groupName": "worker-rs crates",
|
||||
"matchSourceUrls": ["https://github.com/cloudflare/workers-rs"]
|
||||
},
|
||||
{
|
||||
"description": "Group jsonrpsee crates",
|
||||
"groupName": "jsonrpsee crates",
|
||||
"matchSourceUrls": ["https://github.com/paritytech/jsonrpsee"]
|
||||
},
|
||||
{
|
||||
"description": "Bump CLI packages",
|
||||
"matchFileNames": [
|
||||
|
||||
Reference in new issue
Block a user