mirror of
https://github.com/tauri-apps/tauri.git
synced 2026-04-11 10:43:31 +02:00
Compare commits
190 Commits
@tauri-app
...
fix/mobile
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70bcaaace9 | ||
|
|
dd7e59a495 | ||
|
|
2d2a1be429 | ||
|
|
afdd288eab | ||
|
|
79a7d9ec01 | ||
|
|
f855caf8a3 | ||
|
|
ee3cc4a91b | ||
|
|
b5ef603d84 | ||
|
|
ce98d87ce0 | ||
|
|
ad1dec2e24 | ||
|
|
beffcd880f | ||
|
|
956031d73d | ||
|
|
4b00130b86 | ||
|
|
8e3bd63db9 | ||
|
|
cfe47871a5 | ||
|
|
236f55b7aa | ||
|
|
9bb7e79e97 | ||
|
|
d566679a99 | ||
|
|
3899d456d4 | ||
|
|
b586ecf1f4 | ||
|
|
dd70d213cd | ||
|
|
d06a1994e9 | ||
|
|
b446a858de | ||
|
|
85ba5315c2 | ||
|
|
779612ac84 | ||
|
|
22edc65aad | ||
|
|
9a19226369 | ||
|
|
fd8c30b4f1 | ||
|
|
18464d9481 | ||
|
|
b80f9deb5f | ||
|
|
1afa9df6d5 | ||
|
|
75a1fec705 | ||
|
|
100dc94c48 | ||
|
|
7f710b8f3b | ||
|
|
bda1d22369 | ||
|
|
28b9e7c7b8 | ||
|
|
3056d44d96 | ||
|
|
fc017ee257 | ||
|
|
67c7418c06 | ||
|
|
f59bf9d539 | ||
|
|
4b6b8690ab | ||
|
|
cdc5594286 | ||
|
|
a1c231ec29 | ||
|
|
752c923002 | ||
|
|
cb28f4368c | ||
|
|
6aa7f2d852 | ||
|
|
06f26bbb24 | ||
|
|
68cb318979 | ||
|
|
3397fd9bfe | ||
|
|
3b4fac2017 | ||
|
|
684791efa6 | ||
|
|
25e920e169 | ||
|
|
a279485856 | ||
|
|
7b0d4e7322 | ||
|
|
c5008b829d | ||
|
|
b5aa018702 | ||
|
|
55453e8453 | ||
|
|
75082cc5b3 | ||
|
|
006d592837 | ||
|
|
d2938486e9 | ||
|
|
19fb6f7cb0 | ||
|
|
3d6868d09c | ||
|
|
cc8c0b5317 | ||
|
|
20e53a4b95 | ||
|
|
08bda64c25 | ||
|
|
28a2f9bc55 | ||
|
|
ed7c9a4100 | ||
|
|
abf7e8850b | ||
|
|
b0012424c5 | ||
|
|
06d4a4ed6c | ||
|
|
a99601ee4b | ||
|
|
2e089f6acb | ||
|
|
6bbb530fd5 | ||
|
|
b06b3bd091 | ||
|
|
eb60b9966b | ||
|
|
94cbd40fc7 | ||
|
|
673867aa0e | ||
|
|
4188ffdafc | ||
|
|
12a6787110 | ||
|
|
6cb73194c4 | ||
|
|
d1892b97ce | ||
|
|
e446926a6a | ||
|
|
b0c493a4ea | ||
|
|
d340b8c8b1 | ||
|
|
830146d0be | ||
|
|
fa3771b7bc | ||
|
|
9efe474e06 | ||
|
|
69476d8e23 | ||
|
|
f5851ee00d | ||
|
|
66cb1dbbef | ||
|
|
a58d461eb0 | ||
|
|
2a06d10066 | ||
|
|
59089723fc | ||
|
|
1a6627ee7d | ||
|
|
f6622a3e34 | ||
|
|
80eadb7387 | ||
|
|
346a420812 | ||
|
|
5239d39149 | ||
|
|
0b1da30d28 | ||
|
|
7db7142f9f | ||
|
|
a9b342125d | ||
|
|
bcf000c0a8 | ||
|
|
61b9b681e8 | ||
|
|
c37a298331 | ||
|
|
b8b866fcc7 | ||
|
|
956b4fd6ff | ||
|
|
07e134f70e | ||
|
|
f70b28529d | ||
|
|
c23bec62d6 | ||
|
|
9a35a616f5 | ||
|
|
755eb33d1c | ||
|
|
df61fac2b5 | ||
|
|
16348ac2bd | ||
|
|
03e7c11932 | ||
|
|
e81635aa3d | ||
|
|
0ac89d3b6c | ||
|
|
4791d09a0a | ||
|
|
bc829ee24d | ||
|
|
11800a0071 | ||
|
|
662b39adb3 | ||
|
|
2aaa801c35 | ||
|
|
5349984064 | ||
|
|
5f535b4150 | ||
|
|
f3df96fb38 | ||
|
|
c0d3f9d47e | ||
|
|
d54f3b95a6 | ||
|
|
1e7aac355f | ||
|
|
8d869717da | ||
|
|
f0172a454a | ||
|
|
5075b67d36 | ||
|
|
c3252f72f6 | ||
|
|
b4abb6cae8 | ||
|
|
1a3d1a024e | ||
|
|
37154ebdcd | ||
|
|
380656874e | ||
|
|
bc4afe7dd4 | ||
|
|
7c2eb31c83 | ||
|
|
737364b8d3 | ||
|
|
68874c68c5 | ||
|
|
dfadcb764b | ||
|
|
22d6bcacbb | ||
|
|
b21d86a8a3 | ||
|
|
33d0b3f0c1 | ||
|
|
f1232671ab | ||
|
|
0c402bfb6b | ||
|
|
d6d5f37077 | ||
|
|
7261a14368 | ||
|
|
0e6b5cbe5f | ||
|
|
a3dc42477a | ||
|
|
21ebc6e820 | ||
|
|
2d5f5a9230 | ||
|
|
4475e93e13 | ||
|
|
5110a762e9 | ||
|
|
a9ec12843a | ||
|
|
f0dcf9637c | ||
|
|
196ace3c04 | ||
|
|
82e264552e | ||
|
|
c134a769ea | ||
|
|
390cb9c36a | ||
|
|
9300b59f65 | ||
|
|
e1d7be8e57 | ||
|
|
90c1c327ac | ||
|
|
83032e273b | ||
|
|
a8f1569b04 | ||
|
|
0ea08e901e | ||
|
|
887b8da684 | ||
|
|
7d21e3b2fa | ||
|
|
4d270a96a8 | ||
|
|
bcc7a82a3a | ||
|
|
8b465a12ba | ||
|
|
ee68c918a1 | ||
|
|
d7075b66bd | ||
|
|
bbcea1f5e8 | ||
|
|
5ba1c3faa4 | ||
|
|
e27427f795 | ||
|
|
a32a4ce3be | ||
|
|
bc6b125b24 | ||
|
|
9c938be452 | ||
|
|
5c8182860c | ||
|
|
1d31e4647c | ||
|
|
517e7b60e1 | ||
|
|
72b4226ee9 | ||
|
|
d6d941c3a7 | ||
|
|
a0113a8c64 | ||
|
|
91508c0b8d | ||
|
|
fd63f229d5 | ||
|
|
af95fb6014 | ||
|
|
65bb24b9ae | ||
|
|
332ec355a1 | ||
|
|
2c46b1873e |
5
.changes/add-plugin-listener-error-handling.md
Normal file
5
.changes/add-plugin-listener-error-handling.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@tauri-apps/api": patch:bug
|
||||
---
|
||||
|
||||
Fix `addPluginListener` fallback added in https://github.com/tauri-apps/tauri/pull/14132 didn't work properly
|
||||
@@ -27,12 +27,6 @@
|
||||
"dryRunCommand": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo generate-lockfile",
|
||||
"dryRunCommand": true,
|
||||
"runFromRoot": true,
|
||||
"pipe": true
|
||||
},
|
||||
{
|
||||
"command": "cargo audit ${ process.env.CARGO_AUDIT_OPTIONS || '' }",
|
||||
"dryRunCommand": true,
|
||||
|
||||
6
.changes/fix-cli-options-mobile.md
Normal file
6
.changes/fix-cli-options-mobile.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tauri-apps/cli": patch:bug
|
||||
"tauri-cli": patch:bug
|
||||
---
|
||||
|
||||
Fixes Cargo features and args not being applied to the first cargo build calls of `[android|ios] [dev|build]` commands.
|
||||
8
.changes/fix-needles-collect.md
Normal file
8
.changes/fix-needles-collect.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"tauri": patch:perf
|
||||
"tauri-cli": patch:perf
|
||||
"tauri-bundler": patch:perf
|
||||
"@tauri-apps/cli": patch:perf
|
||||
---
|
||||
|
||||
refactor: remove needless collect. No user facing changes.
|
||||
5
.changes/nsis-3.11.md
Normal file
5
.changes/nsis-3.11.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"tauri-bundler": patch:deps
|
||||
---
|
||||
|
||||
Updated NSIS from 3.8 to 3.11
|
||||
8
.changes/perf-remove-needless-clone.md
Normal file
8
.changes/perf-remove-needless-clone.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"tauri-bundler": patch:perf
|
||||
"tauri-cli": patch:perf
|
||||
"tauri-macos-sign": patch:perf
|
||||
"tauri": patch:perf
|
||||
---
|
||||
|
||||
perf: remove needless clones in various files for improved performance. No user facing changes.
|
||||
6
.changes/pnpm-package-version-check.md
Normal file
6
.changes/pnpm-package-version-check.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"tauri-cli": patch:bug
|
||||
"@tauri-apps/cli": patch:bug
|
||||
---
|
||||
|
||||
Fixed the mismatched tauri package versions check didn't work for pnpm
|
||||
5
.changes/version-req-error.md
Normal file
5
.changes/version-req-error.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
tauri-cli: patch:bug
|
||||
---
|
||||
|
||||
Fixed an issue that caused the cli to print errors like `Error Failed to parse version 2 for crate tauri` when there was no `Cargo.lock` file present yet. This will still be logged in `--verbose` mode.
|
||||
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
@@ -33,11 +33,9 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
|
||||
- It's OK to have multiple small commits as you work on the PR - we will let GitHub automatically squash it before merging.
|
||||
|
||||
- If adding new feature:
|
||||
|
||||
- Provide convincing reason to add this feature. Ideally you should open a suggestion issue first and have it greenlighted before working on it.
|
||||
|
||||
- If fixing a bug:
|
||||
|
||||
- If you are resolving a special issue, add `(fix: #xxxx[,#xxx])` (#xxxx is the issue id) in your PR title for a better release log, e.g. `fix: update entities encoding/decoding (fix #3899)`.
|
||||
- Provide detailed description of the bug in the PR, or link to an issue that does.
|
||||
|
||||
@@ -98,7 +96,7 @@ You can use `cargo install --path . --debug` to speed up test builds.
|
||||
You can build the Rust documentation locally running the following script:
|
||||
|
||||
```bash
|
||||
$ RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --open
|
||||
$ cargo +nightly doc --all-features --open
|
||||
```
|
||||
|
||||
### Developing the JS API
|
||||
|
||||
@@ -78,7 +78,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: cargo login
|
||||
run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }}
|
||||
|
||||
4
.github/workflows/test-core.yml
vendored
4
.github/workflows/test-core.yml
vendored
@@ -95,10 +95,10 @@ jobs:
|
||||
|
||||
- name: test
|
||||
if: ${{ !matrix.platform.cross }}
|
||||
run: cargo ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
run: cargo ${{ matrix.features.key == 'no-default' && 'check' || matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
|
||||
- name: test (using cross)
|
||||
if: ${{ matrix.platform.cross }}
|
||||
run: |
|
||||
cargo install cross --git https://github.com/cross-rs/cross --rev 51f46f296253d8122c927c5bb933e3c4f27cc317 --locked
|
||||
cross ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
cross ${{ matrix.features.key == 'no-default' && 'check' || matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
|
||||
|
||||
@@ -57,7 +57,7 @@ function checkChangeFiles(changeFiles) {
|
||||
for (const [file, packages] of unknownTagsEntries) {
|
||||
for (const { package, tag } of packages) {
|
||||
console.error(
|
||||
`Package \`${package}\` has an uknown change tag ${tag} in ${file} `
|
||||
`Package \`${package}\` has an unknown change tag ${tag} in ${file} `
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const ignore = [
|
||||
async function checkFile(file) {
|
||||
if (
|
||||
extensions.some((e) => file.endsWith(e))
|
||||
&& !ignore.some((i) => file.includes(`/${i}/`) || path.basename(file) == i)
|
||||
&& !ignore.some((i) => file.includes(`/${i}/`) || path.basename(file) === i)
|
||||
) {
|
||||
const fileStream = fs.createReadStream(file)
|
||||
const rl = readline.createInterface({
|
||||
|
||||
308
Cargo.lock
generated
308
Cargo.lock
generated
@@ -1055,9 +1055,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo-mobile2"
|
||||
version = "0.20.2"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f2e0347234eb5a7b47eb66d33dc18560a628f031dffe58c37a7efe44b53e6f9"
|
||||
checksum = "dcea7efeaac9f0fd9f886f43a13dde186a1e2266fe6b53a42659e4e0689570de"
|
||||
dependencies = [
|
||||
"colored",
|
||||
"core-foundation 0.10.0",
|
||||
@@ -1083,9 +1083,9 @@ dependencies = [
|
||||
"serde_json",
|
||||
"textwrap",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.9.0",
|
||||
"toml 0.9.4",
|
||||
"ureq",
|
||||
"which 8.0.0",
|
||||
"which",
|
||||
"windows 0.61.1",
|
||||
"x509-certificate 0.24.0",
|
||||
]
|
||||
@@ -1115,12 +1115,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cargo_toml"
|
||||
version = "0.22.1"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257"
|
||||
checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1319,7 +1319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1641,6 +1641,12 @@ dependencies = [
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ct-codecs"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.2.9"
|
||||
@@ -1994,9 +2000,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dlopen2"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1297103d2bbaea85724fcee6294c2d50b1081f9ad47d0f6f6f61eda65315a6"
|
||||
checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff"
|
||||
dependencies = [
|
||||
"dlopen2_derive",
|
||||
"libc",
|
||||
@@ -2206,16 +2212,16 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "embed-resource"
|
||||
version = "3.0.1"
|
||||
version = "3.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4762ce03154ba57ebaeee60cc631901ceae4f18219cbb874e464347471594742"
|
||||
checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"memchr",
|
||||
"rustc_version",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.4",
|
||||
"vswhom",
|
||||
"winreg 0.52.0",
|
||||
"winreg 0.55.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2362,9 +2368,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.14.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
|
||||
checksum = "bf04c5ec15464ace8355a7b440a33aece288993475556d461154d7a62ad9947c"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
@@ -2436,18 +2442,18 @@ dependencies = [
|
||||
"atomic",
|
||||
"pear",
|
||||
"serde",
|
||||
"toml 0.8.19",
|
||||
"toml 0.8.20",
|
||||
"uncased",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "file-id"
|
||||
version = "0.2.2"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6bc904b9bbefcadbd8e3a9fb0d464a9b979de6324c03b3c663e8994f46a5be36"
|
||||
checksum = "e1fc6a637b6dc58414714eddd9170ff187ecb0933d4c7024d1abbd23a3cc26e9"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2860,9 +2866,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3907,6 +3915,20 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"combine",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
@@ -4098,9 +4120,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
|
||||
checksum = "d46662859bc5f60a145b75f4632fbadc84e829e45df6c5de74cfc8e05acb96b5"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"base64 0.22.1",
|
||||
@@ -4185,9 +4207,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.0.8"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
@@ -4297,7 +4319,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4578,11 +4600,12 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "minisign"
|
||||
version = "0.7.3"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b23ef13ff1d745b1e52397daaa247e333c607f3cff96d4df2b798dc252db974b"
|
||||
checksum = "e6bf96cef396a17a96f7600281aa4da9229860b7a082601b1f6db6eaa5f99ee5"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"ct-codecs",
|
||||
"getrandom 0.3.3",
|
||||
"rpassword",
|
||||
"scrypt",
|
||||
]
|
||||
@@ -4611,9 +4634,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "muda"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58b89bf91c19bf036347f1ab85a81c560f08c0667c8601bece664d860a600988"
|
||||
checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a"
|
||||
dependencies = [
|
||||
"crossbeam-channel",
|
||||
"dpi",
|
||||
@@ -4628,7 +4651,7 @@ dependencies = [
|
||||
"png",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4813,12 +4836,11 @@ checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
@@ -4827,14 +4849,14 @@ dependencies = [
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-debouncer-full"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2d88b1a7538054351c8258338df7c931a590513fb3745e8c15eb9ff4199b8d1"
|
||||
checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078"
|
||||
dependencies = [
|
||||
"file-id",
|
||||
"log",
|
||||
@@ -6168,7 +6190,7 @@ version = "3.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b"
|
||||
dependencies = [
|
||||
"toml_edit 0.22.22",
|
||||
"toml_edit 0.22.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6702,9 +6724,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.30.0"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
|
||||
checksum = "9e9c261f7ce75418b3beadfb3f0eb1299fe8eb9640deba45ffa2cb783098697d"
|
||||
dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"fluent-uri",
|
||||
@@ -6963,13 +6985,13 @@ checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||
|
||||
[[package]]
|
||||
name = "rpassword"
|
||||
version = "7.3.1"
|
||||
version = "7.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
||||
checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rtoolbox",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7211,6 +7233,33 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490"
|
||||
dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"jni 0.19.0",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rustls 0.23.20",
|
||||
"rustls-native-certs 0.7.3",
|
||||
"rustls-platform-verifier-android",
|
||||
"rustls-webpki 0.102.8",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-roots 0.26.7",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-platform-verifier-android"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
@@ -7430,6 +7479,7 @@ dependencies = [
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"num-bigint",
|
||||
"security-framework-sys",
|
||||
]
|
||||
|
||||
@@ -7478,9 +7528,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.217"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@@ -7531,9 +7581,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.217"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -7694,9 +7744,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb"
|
||||
checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -7705,13 +7755,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript-impl"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763"
|
||||
checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.95",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8364,17 +8414,18 @@ dependencies = [
|
||||
"cfg-expr",
|
||||
"heck 0.5.0",
|
||||
"pkg-config",
|
||||
"toml 0.8.19",
|
||||
"toml 0.8.20",
|
||||
"version-compare",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.34.0"
|
||||
version = "0.34.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49c380ca75a231b87b6c9dd86948f035012e7171d1a7c40a9c2890489a7ffd8a"
|
||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
dependencies = [
|
||||
"bitflags 2.7.0",
|
||||
"block2 0.6.0",
|
||||
"core-foundation 0.10.0",
|
||||
"core-graphics",
|
||||
"crossbeam-channel",
|
||||
@@ -8384,7 +8435,7 @@ dependencies = [
|
||||
"gdkwayland-sys",
|
||||
"gdkx11-sys",
|
||||
"gtk",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -8443,11 +8494,12 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.7.0"
|
||||
version = "2.9.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"cargo_toml",
|
||||
"cookie",
|
||||
"data-url",
|
||||
"dirs 6.0.0",
|
||||
"dunce",
|
||||
@@ -8459,7 +8511,7 @@ dependencies = [
|
||||
"http 1.3.1",
|
||||
"http-range",
|
||||
"image",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"libc",
|
||||
"log",
|
||||
"mime",
|
||||
@@ -8493,7 +8545,6 @@ dependencies = [
|
||||
"tracing",
|
||||
"tray-icon",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
@@ -8503,7 +8554,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.3.1"
|
||||
version = "2.5.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -8519,13 +8570,13 @@ dependencies = [
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
"tauri-winres",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.4",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-bundler"
|
||||
version = "2.5.1"
|
||||
version = "2.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
@@ -8563,7 +8614,7 @@ dependencies = [
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which 7.0.1",
|
||||
"which",
|
||||
"windows-registry 0.5.0",
|
||||
"windows-sys 0.60.2",
|
||||
"zip 4.0.0",
|
||||
@@ -8571,9 +8622,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-cli"
|
||||
version = "2.7.0"
|
||||
version = "2.9.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"ar",
|
||||
"axum",
|
||||
"base64 0.22.1",
|
||||
@@ -8585,6 +8635,7 @@ dependencies = [
|
||||
"css-color",
|
||||
"ctrlc",
|
||||
"dialoguer",
|
||||
"dirs 6.0.0",
|
||||
"duct",
|
||||
"dunce",
|
||||
"elf",
|
||||
@@ -8625,6 +8676,7 @@ dependencies = [
|
||||
"plist",
|
||||
"pretty_assertions",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"regex",
|
||||
"resvg",
|
||||
"semver",
|
||||
@@ -8638,14 +8690,17 @@ dependencies = [
|
||||
"tauri-macos-sign",
|
||||
"tauri-utils",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"toml 0.8.19",
|
||||
"toml_edit 0.22.22",
|
||||
"toml 0.9.4",
|
||||
"toml_edit 0.23.2",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
"walkdir",
|
||||
"which",
|
||||
"windows-sys 0.60.2",
|
||||
"zip 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8661,7 +8716,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.3.1"
|
||||
version = "2.5.1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"brotli",
|
||||
@@ -8700,7 +8755,7 @@ dependencies = [
|
||||
"signal-hook",
|
||||
"signal-hook-tokio",
|
||||
"tokio",
|
||||
"which 7.0.1",
|
||||
"which",
|
||||
"win32job",
|
||||
]
|
||||
|
||||
@@ -8727,9 +8782,8 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macos-sign"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"apple-codesign",
|
||||
"chrono",
|
||||
"dirs 6.0.0",
|
||||
@@ -8742,12 +8796,13 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"x509-certificate 0.23.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.3.2"
|
||||
version = "2.5.1"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
@@ -8759,7 +8814,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin"
|
||||
version = "2.3.1"
|
||||
version = "2.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"glob",
|
||||
@@ -8768,15 +8823,15 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.4",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.4.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d2b582d860eb214f28323f4ce4f2797ae3b78f197e27b11677f976f9f52aedb"
|
||||
checksum = "a59139183e0907cec1499dddee4e085f5a801dc659efa0848ee224f461371426"
|
||||
dependencies = [
|
||||
"android_logger",
|
||||
"byte-unit",
|
||||
@@ -8807,31 +8862,34 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.7.1"
|
||||
version = "2.9.1"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"dpi",
|
||||
"gtk",
|
||||
"http 1.3.1",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"objc2 0.6.0",
|
||||
"objc2-ui-kit",
|
||||
"objc2-web-kit",
|
||||
"raw-window-handle",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.12",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
"webview2-com",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.7.2"
|
||||
version = "2.9.1"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"http 1.3.1",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"log",
|
||||
"objc2 0.6.0",
|
||||
"objc2-app-kit",
|
||||
@@ -8878,7 +8936,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.6.0"
|
||||
version = "2.8.0"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"anyhow",
|
||||
@@ -8910,7 +8968,7 @@ dependencies = [
|
||||
"serialize-to-javascript",
|
||||
"swift-rs",
|
||||
"thiserror 2.0.12",
|
||||
"toml 0.8.19",
|
||||
"toml 0.9.4",
|
||||
"url",
|
||||
"urlpattern",
|
||||
"uuid",
|
||||
@@ -8919,12 +8977,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-winres"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56eaa45f707bedf34d19312c26d350bc0f3c59a47e58e8adbeecdc850d2c13a0"
|
||||
checksum = "7c6d9028d41d4de835e3c482c677a8cb88137ac435d6ff9a71f392d4421576c9"
|
||||
dependencies = [
|
||||
"embed-resource",
|
||||
"toml 0.8.19",
|
||||
"indexmap 2.7.0",
|
||||
"toml 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9241,21 +9300,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
version = "0.8.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned 0.6.8",
|
||||
"toml_datetime 0.6.8",
|
||||
"toml_edit 0.22.22",
|
||||
"toml_edit 0.22.24",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.0"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f271e09bde39ab52250160a67e88577e0559ad77e9085de6e9051a2c4353f8f8"
|
||||
checksum = "41ae868b5a0f67631c14589f7e250c1ea2c574ee5ba21c6c8dd4b1485705a5a1"
|
||||
dependencies = [
|
||||
"indexmap 2.7.0",
|
||||
"serde",
|
||||
@@ -9308,31 +9367,46 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.22"
|
||||
version = "0.22.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
|
||||
dependencies = [
|
||||
"indexmap 2.7.0",
|
||||
"serde",
|
||||
"serde_spanned 0.6.8",
|
||||
"toml_datetime 0.6.8",
|
||||
"winnow 0.6.22",
|
||||
"winnow 0.7.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.23.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1dee9dc43ac2aaf7d3b774e2fba5148212bf2bd9374f4e50152ebe9afd03d42"
|
||||
dependencies = [
|
||||
"indexmap 2.7.0",
|
||||
"serde",
|
||||
"serde_spanned 1.0.0",
|
||||
"toml_datetime 0.7.0",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow 0.7.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5c1c469eda89749d2230d8156a5969a69ffe0d6d01200581cdc6110674d293e"
|
||||
checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
|
||||
dependencies = [
|
||||
"winnow 0.7.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.0"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b679217f2848de74cabd3e8fc5e6d66f40b7da40f8e1954d92054d9010690fd5"
|
||||
checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
@@ -9718,6 +9792,7 @@ dependencies = [
|
||||
"rustls 0.23.20",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
"rustls-platform-verifier",
|
||||
"socks",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
@@ -10200,18 +10275,6 @@ version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "7.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb4a9e33648339dc1642b0e36e21b3385e6148e289226f657c809dee59df5028"
|
||||
dependencies = [
|
||||
"either",
|
||||
"env_home",
|
||||
"rustix 0.38.43",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.0"
|
||||
@@ -10261,7 +10324,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10760,20 +10823,14 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39281189af81c07ec09db316b302a3e67bf9bd7cbf6c820b50e35fee9c2fa980"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
@@ -10787,12 +10844,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.52.0"
|
||||
version = "0.55.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10899,14 +10956,15 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "wry"
|
||||
version = "0.52.1"
|
||||
version = "0.53.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12a714d9ba7075aae04a6e50229d6109e3d584774b99a6a8c60de1698ca111b9"
|
||||
checksum = "6d78ec082b80fa088569a970d043bb3050abaabf4454101d44514ee8d9a8c9f6"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"block2 0.6.0",
|
||||
"cookie",
|
||||
"crossbeam-channel",
|
||||
"dirs 6.0.0",
|
||||
"dpi",
|
||||
"dunce",
|
||||
"gdkx11",
|
||||
@@ -10914,7 +10972,7 @@ dependencies = [
|
||||
"html5ever",
|
||||
"http 1.3.1",
|
||||
"javascriptcore-rs",
|
||||
"jni",
|
||||
"jni 0.21.1",
|
||||
"kuchikiki",
|
||||
"libc",
|
||||
"ndk",
|
||||
|
||||
@@ -71,3 +71,4 @@ opt-level = "s"
|
||||
schemars_derive = { git = 'https://github.com/tauri-apps/schemars.git', branch = 'feat/preserve-description-newlines' }
|
||||
tauri = { path = "./crates/tauri" }
|
||||
tauri-plugin = { path = "./crates/tauri-plugin" }
|
||||
tauri-utils = { path = "./crates/tauri-utils" }
|
||||
|
||||
@@ -81,7 +81,7 @@ For the complete list of sponsors please visit our [website](https://tauri.app#s
|
||||
|
||||
## Organization
|
||||
|
||||
Tauri aims to be a sustainable collective based on principles that guide [sustainable free and open software communities](https://sfosc.org). To this end it has become a Programme within the [Commons Conservancy](https://commonsconservancy.org/), and you can contribute financially via [Open Collective](https://opencollective.com/tauri).
|
||||
Tauri aims to be a sustainable collective based on principles that guide sustainable free and open software communities. To this end it has become a Programme within the [Commons Conservancy](https://commonsconservancy.org/), and you can contribute financially via [Open Collective](https://opencollective.com/tauri).
|
||||
|
||||
## Licenses
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
// file is used by multiple binaries
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::{fs::File, io::BufReader};
|
||||
mod utils;
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! This Rust binary runs on CI and provides internal metrics results of Tauri. To learn more see [benchmark_results](https://github.com/tauri-apps/benchmark_results) repository.
|
||||
//! This Rust binary runs on CI and provides internal metrics results of Tauri.
|
||||
//! To learn more see [benchmark_results](https://github.com/tauri-apps/benchmark_results) repository.
|
||||
//!
|
||||
//! ***_Internal use only_**
|
||||
//! ***_Internal use only_***
|
||||
|
||||
#![doc(
|
||||
html_logo_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png",
|
||||
html_favicon_url = "https://github.com/tauri-apps/tauri/raw/dev/.github/icon.png"
|
||||
)]
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
env,
|
||||
@@ -21,62 +22,66 @@ use std::{
|
||||
|
||||
mod utils;
|
||||
|
||||
/// The list of the examples of the benchmark name and binary relative path
|
||||
fn get_all_benchmarks() -> Vec<(String, String)> {
|
||||
/// The list of examples for benchmarks
|
||||
fn get_all_benchmarks(target: &str) -> Vec<(String, String)> {
|
||||
vec![
|
||||
(
|
||||
"tauri_hello_world".into(),
|
||||
format!("../target/{}/release/bench_helloworld", utils::get_target()),
|
||||
format!("../target/{target}/release/bench_helloworld"),
|
||||
),
|
||||
(
|
||||
"tauri_cpu_intensive".into(),
|
||||
format!(
|
||||
"../target/{}/release/bench_cpu_intensive",
|
||||
utils::get_target()
|
||||
),
|
||||
format!("../target/{target}/release/bench_cpu_intensive"),
|
||||
),
|
||||
(
|
||||
"tauri_3mb_transfer".into(),
|
||||
format!(
|
||||
"../target/{}/release/bench_files_transfer",
|
||||
utils::get_target()
|
||||
),
|
||||
format!("../target/{target}/release/bench_files_transfer"),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn run_strace_benchmarks(new_data: &mut utils::BenchResult) -> Result<()> {
|
||||
fn run_strace_benchmarks(new_data: &mut utils::BenchResult, target: &str) -> Result<()> {
|
||||
use std::io::Read;
|
||||
|
||||
let mut thread_count = HashMap::<String, u64>::new();
|
||||
let mut syscall_count = HashMap::<String, u64>::new();
|
||||
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let mut file = tempfile::NamedTempFile::new()
|
||||
.context("failed to create temporary file for strace output")?;
|
||||
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
let temp_path_str = file
|
||||
.path()
|
||||
.to_str()
|
||||
.context("temporary file path contains invalid UTF-8")?;
|
||||
|
||||
Command::new("strace")
|
||||
.args([
|
||||
"-c",
|
||||
"-f",
|
||||
"-o",
|
||||
file.path().to_str().unwrap(),
|
||||
utils::bench_root_path().join(example_exe).to_str().unwrap(),
|
||||
])
|
||||
.args(["-c", "-f", "-o", temp_path_str, exe_path_str])
|
||||
.stdout(Stdio::inherit())
|
||||
.spawn()?
|
||||
.wait()?;
|
||||
.spawn()
|
||||
.context("failed to spawn strace process")?
|
||||
.wait()
|
||||
.context("failed to wait for strace process")?;
|
||||
|
||||
let mut output = String::new();
|
||||
file.as_file_mut().read_to_string(&mut output)?;
|
||||
file
|
||||
.as_file_mut()
|
||||
.read_to_string(&mut output)
|
||||
.context("failed to read strace output")?;
|
||||
|
||||
let strace_result = utils::parse_strace_output(&output);
|
||||
// Note, we always have 1 thread. Use cloneX calls as counter for additional threads created.
|
||||
let clone = 1
|
||||
+ strace_result.get("clone").map(|d| d.calls).unwrap_or(0)
|
||||
// Count clone/clone3 syscalls as thread creation indicators
|
||||
let clone_calls = strace_result.get("clone").map(|d| d.calls).unwrap_or(0)
|
||||
+ strace_result.get("clone3").map(|d| d.calls).unwrap_or(0);
|
||||
let total = strace_result.get("total").unwrap().calls;
|
||||
thread_count.insert(name.to_string(), clone);
|
||||
syscall_count.insert(name.to_string(), total);
|
||||
|
||||
if let Some(total) = strace_result.get("total") {
|
||||
thread_count.insert(name.clone(), clone_calls);
|
||||
syscall_count.insert(name, total.calls);
|
||||
}
|
||||
}
|
||||
|
||||
new_data.thread_count = thread_count;
|
||||
@@ -85,70 +90,100 @@ fn run_strace_benchmarks(new_data: &mut utils::BenchResult) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_max_mem_benchmark() -> Result<HashMap<String, u64>> {
|
||||
fn run_max_mem_benchmark(target: &str) -> Result<HashMap<String, u64>> {
|
||||
let mut results = HashMap::<String, u64>::new();
|
||||
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let benchmark_file = utils::target_dir().join(format!("mprof{name}_.dat"));
|
||||
let benchmark_file = benchmark_file.to_str().unwrap();
|
||||
let benchmark_file_str = benchmark_file
|
||||
.to_str()
|
||||
.context("benchmark file path contains invalid UTF-8")?;
|
||||
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
|
||||
let proc = Command::new("mprof")
|
||||
.args([
|
||||
"run",
|
||||
"-C",
|
||||
"-o",
|
||||
benchmark_file,
|
||||
utils::bench_root_path().join(example_exe).to_str().unwrap(),
|
||||
])
|
||||
.args(["run", "-C", "-o", benchmark_file_str, exe_path_str])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()?;
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to spawn mprof for benchmark {name}"))?;
|
||||
|
||||
let proc_result = proc.wait_with_output()?;
|
||||
println!("{proc_result:?}");
|
||||
results.insert(
|
||||
name.to_string(),
|
||||
utils::parse_max_mem(benchmark_file).unwrap(),
|
||||
);
|
||||
let proc_result = proc
|
||||
.wait_with_output()
|
||||
.with_context(|| format!("failed to wait for mprof {name}"))?;
|
||||
|
||||
if !proc_result.status.success() {
|
||||
eprintln!(
|
||||
"mprof failed for {name}: {}",
|
||||
String::from_utf8_lossy(&proc_result.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(mem) = utils::parse_max_mem(benchmark_file_str)
|
||||
.with_context(|| format!("failed to parse mprof data for {name}"))?
|
||||
{
|
||||
results.insert(name, mem);
|
||||
}
|
||||
|
||||
// Clean up the temporary file
|
||||
if let Err(e) = std::fs::remove_file(&benchmark_file) {
|
||||
eprintln!("Warning: failed to remove temporary file {benchmark_file_str}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn rlib_size(target_dir: &std::path::Path, prefix: &str) -> u64 {
|
||||
fn rlib_size(target_dir: &Path, prefix: &str) -> Result<u64> {
|
||||
let mut size = 0;
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
let deps_dir = target_dir.join("deps");
|
||||
for entry in std::fs::read_dir(&deps_dir).with_context(|| {
|
||||
format!(
|
||||
"failed to read target deps directory: {}",
|
||||
deps_dir.display()
|
||||
)
|
||||
})? {
|
||||
let entry = entry.context("failed to read directory entry")?;
|
||||
let name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
for entry in std::fs::read_dir(target_dir.join("deps")).unwrap() {
|
||||
let entry = entry.unwrap();
|
||||
let os_str = entry.file_name();
|
||||
let name = os_str.to_str().unwrap();
|
||||
if name.starts_with(prefix) && name.ends_with(".rlib") {
|
||||
let start = name.split('-').next().unwrap().to_string();
|
||||
if seen.contains(&start) {
|
||||
println!("skip {name}");
|
||||
} else {
|
||||
seen.insert(start);
|
||||
size += entry.metadata().unwrap().len();
|
||||
println!("check size {name} {size}");
|
||||
if let Some(start) = name.split('-').next() {
|
||||
if seen.insert(start.to_string()) {
|
||||
size += entry
|
||||
.metadata()
|
||||
.context("failed to read file metadata")?
|
||||
.len();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(size > 0);
|
||||
size
|
||||
|
||||
if size == 0 {
|
||||
anyhow::bail!(
|
||||
"no rlib files found for prefix {prefix} in {}",
|
||||
deps_dir.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn get_binary_sizes(target_dir: &Path) -> Result<HashMap<String, u64>> {
|
||||
fn get_binary_sizes(target_dir: &Path, target: &str) -> Result<HashMap<String, u64>> {
|
||||
let mut sizes = HashMap::<String, u64>::new();
|
||||
|
||||
let wry_size = rlib_size(target_dir, "libwry");
|
||||
println!("wry {wry_size} bytes");
|
||||
let wry_size = rlib_size(target_dir, "libwry")?;
|
||||
sizes.insert("wry_rlib".to_string(), wry_size);
|
||||
|
||||
// add size for all EXEC_TIME_BENCHMARKS
|
||||
for (name, example_exe) in get_all_benchmarks() {
|
||||
let meta = std::fs::metadata(example_exe).unwrap();
|
||||
sizes.insert(name.to_string(), meta.len());
|
||||
for (name, example_exe) in get_all_benchmarks(target) {
|
||||
let exe_path = utils::bench_root_path().join(&example_exe);
|
||||
let meta = std::fs::metadata(&exe_path)
|
||||
.with_context(|| format!("failed to read metadata for {}", exe_path.display()))?;
|
||||
sizes.insert(name, meta.len());
|
||||
}
|
||||
|
||||
Ok(sizes)
|
||||
@@ -188,14 +223,33 @@ fn cargo_deps() -> HashMap<String, usize> {
|
||||
cmd.args(["--target", target]);
|
||||
cmd.current_dir(utils::tauri_root_path());
|
||||
|
||||
let full_deps = cmd.output().expect("failed to run cargo tree").stdout;
|
||||
let full_deps = String::from_utf8(full_deps).expect("cargo tree output not utf-8");
|
||||
let count = full_deps.lines().collect::<HashSet<_>>().len() - 1; // output includes wry itself
|
||||
match cmd.output() {
|
||||
Ok(output) if output.status.success() => {
|
||||
let full_deps = String::from_utf8_lossy(&output.stdout);
|
||||
let count = full_deps
|
||||
.lines()
|
||||
.collect::<HashSet<_>>()
|
||||
.len()
|
||||
.saturating_sub(1); // output includes wry itself
|
||||
|
||||
// set the count to the highest count seen for this OS
|
||||
let existing = results.entry(os.to_string()).or_default();
|
||||
*existing = count.max(*existing);
|
||||
assert!(count > 10); // sanity check
|
||||
// set the count to the highest count seen for this OS
|
||||
let existing = results.entry(os.to_string()).or_default();
|
||||
*existing = count.max(*existing);
|
||||
|
||||
if count <= 10 {
|
||||
eprintln!("Warning: dependency count for {target} seems low: {count}");
|
||||
}
|
||||
}
|
||||
Ok(output) => {
|
||||
eprintln!(
|
||||
"cargo tree failed for {target}: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to run cargo tree for {target}: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
results
|
||||
@@ -203,104 +257,127 @@ fn cargo_deps() -> HashMap<String, usize> {
|
||||
|
||||
const RESULT_KEYS: &[&str] = &["mean", "stddev", "user", "system", "min", "max"];
|
||||
|
||||
fn run_exec_time(target_dir: &Path) -> Result<HashMap<String, HashMap<String, f64>>> {
|
||||
fn run_exec_time(target: &str) -> Result<HashMap<String, HashMap<String, f64>>> {
|
||||
let target_dir = utils::target_dir();
|
||||
let benchmark_file = target_dir.join("hyperfine_results.json");
|
||||
let benchmark_file = benchmark_file.to_str().unwrap();
|
||||
let benchmark_file_str = benchmark_file
|
||||
.to_str()
|
||||
.context("benchmark file path contains invalid UTF-8")?;
|
||||
|
||||
let mut command = [
|
||||
let mut command = vec![
|
||||
"hyperfine",
|
||||
"--export-json",
|
||||
benchmark_file,
|
||||
benchmark_file_str,
|
||||
"--show-output",
|
||||
"--warmup",
|
||||
"3",
|
||||
]
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
];
|
||||
|
||||
for (_, example_exe) in get_all_benchmarks() {
|
||||
command.push(
|
||||
utils::bench_root_path()
|
||||
.join(example_exe)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
);
|
||||
let benchmarks = get_all_benchmarks(target);
|
||||
let mut benchmark_paths = Vec::new();
|
||||
|
||||
for (_, example_exe) in &benchmarks {
|
||||
let exe_path = utils::bench_root_path().join(example_exe);
|
||||
let exe_path_str = exe_path
|
||||
.to_str()
|
||||
.context("executable path contains invalid UTF-8")?;
|
||||
benchmark_paths.push(exe_path_str.to_string());
|
||||
}
|
||||
|
||||
utils::run(&command.iter().map(|s| s.as_ref()).collect::<Vec<_>>());
|
||||
for path in &benchmark_paths {
|
||||
command.push(path.as_str());
|
||||
}
|
||||
|
||||
utils::run(&command)?;
|
||||
|
||||
let mut results = HashMap::<String, HashMap<String, f64>>::new();
|
||||
let hyperfine_results = utils::read_json(benchmark_file)?;
|
||||
for ((name, _), data) in get_all_benchmarks().iter().zip(
|
||||
hyperfine_results
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.get("results")
|
||||
.unwrap()
|
||||
.as_array()
|
||||
.unwrap(),
|
||||
) {
|
||||
let data = data.as_object().unwrap().clone();
|
||||
results.insert(
|
||||
name.to_string(),
|
||||
data
|
||||
.into_iter()
|
||||
.filter(|(key, _)| RESULT_KEYS.contains(&key.as_str()))
|
||||
.map(|(key, val)| (key, val.as_f64().unwrap()))
|
||||
.collect(),
|
||||
);
|
||||
let hyperfine_results = utils::read_json(benchmark_file_str)?;
|
||||
|
||||
if let Some(results_array) = hyperfine_results
|
||||
.as_object()
|
||||
.and_then(|obj| obj.get("results"))
|
||||
.and_then(|val| val.as_array())
|
||||
{
|
||||
for ((name, _), data) in benchmarks.iter().zip(results_array.iter()) {
|
||||
if let Some(data_obj) = data.as_object() {
|
||||
let filtered_data: HashMap<String, f64> = data_obj
|
||||
.iter()
|
||||
.filter(|(key, _)| RESULT_KEYS.contains(&key.as_str()))
|
||||
.filter_map(|(key, val)| val.as_f64().map(|v| (key.clone(), v)))
|
||||
.collect();
|
||||
|
||||
results.insert(name.clone(), filtered_data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// download big files if not present
|
||||
let json_3mb = utils::home_path().join(".tauri_3mb.json");
|
||||
|
||||
if !json_3mb.exists() {
|
||||
println!("Downloading test data...");
|
||||
utils::download_file(
|
||||
"https://github.com/lemarier/tauri-test/releases/download/v2.0.0/json_3mb.json",
|
||||
json_3mb,
|
||||
);
|
||||
)
|
||||
.context("failed to download test data")?;
|
||||
}
|
||||
|
||||
println!("Starting tauri benchmark");
|
||||
|
||||
let target_dir = utils::target_dir();
|
||||
let target = utils::get_target();
|
||||
|
||||
env::set_current_dir(utils::bench_root_path())?;
|
||||
env::set_current_dir(utils::bench_root_path())
|
||||
.context("failed to set working directory to bench root")?;
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.context("failed to get current time")?;
|
||||
let timestamp = format!("{}", now.as_secs());
|
||||
|
||||
println!("Running execution time benchmarks...");
|
||||
let exec_time = run_exec_time(target)?;
|
||||
|
||||
println!("Getting binary sizes...");
|
||||
let binary_size = get_binary_sizes(&target_dir, target)?;
|
||||
|
||||
println!("Analyzing cargo dependencies...");
|
||||
let cargo_deps = cargo_deps();
|
||||
|
||||
let format =
|
||||
time::format_description::parse("[year]-[month]-[day]T[hour]:[minute]:[second]Z").unwrap();
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let mut new_data = utils::BenchResult {
|
||||
created_at: now.format(&format).unwrap(),
|
||||
sha1: utils::run_collect(&["git", "rev-parse", "HEAD"])
|
||||
.0
|
||||
.trim()
|
||||
.to_string(),
|
||||
exec_time: run_exec_time(&target_dir)?,
|
||||
binary_size: get_binary_sizes(&target_dir)?,
|
||||
cargo_deps: cargo_deps(),
|
||||
created_at: timestamp,
|
||||
sha1: {
|
||||
let output = utils::run_collect(&["git", "rev-parse", "HEAD"])?;
|
||||
output.0.trim().to_string()
|
||||
},
|
||||
exec_time,
|
||||
binary_size,
|
||||
cargo_deps,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
run_strace_benchmarks(&mut new_data)?;
|
||||
new_data.max_memory = run_max_mem_benchmark()?;
|
||||
println!("Running Linux-specific benchmarks...");
|
||||
run_strace_benchmarks(&mut new_data, target)?;
|
||||
new_data.max_memory = run_max_mem_benchmark(target)?;
|
||||
}
|
||||
|
||||
println!("===== <BENCHMARK RESULTS>");
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &new_data)?;
|
||||
serde_json::to_writer_pretty(std::io::stdout(), &new_data)
|
||||
.context("failed to serialize benchmark results")?;
|
||||
println!("\n===== </BENCHMARK RESULTS>");
|
||||
|
||||
if let Some(filename) = target_dir.join("bench.json").to_str() {
|
||||
utils::write_json(filename, &serde_json::to_value(&new_data)?)?;
|
||||
let bench_file = target_dir.join("bench.json");
|
||||
if let Some(filename) = bench_file.to_str() {
|
||||
utils::write_json(filename, &serde_json::to_value(&new_data)?)
|
||||
.context("failed to write benchmark results to file")?;
|
||||
println!("Results written to: {filename}");
|
||||
} else {
|
||||
eprintln!("Cannot write bench.json, path is invalid");
|
||||
eprintln!("Cannot write bench.json, path contains invalid UTF-8");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -2,7 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Result;
|
||||
//! Utility functions for benchmarking tasks in the Tauri project.
|
||||
//!
|
||||
//! This module provides helpers for:
|
||||
//! - Paths to project directories and targets
|
||||
//! - Running and collecting process outputs
|
||||
//! - Parsing memory profiler (`mprof`) and syscall profiler (`strace`) outputs
|
||||
//! - JSON read/write utilities
|
||||
//! - File download utilities (via `curl` or file copy)
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
@@ -13,6 +22,7 @@ use std::{
|
||||
process::{Command, Output, Stdio},
|
||||
};
|
||||
|
||||
/// Holds the results of a benchmark run.
|
||||
#[derive(Default, Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct BenchResult {
|
||||
pub created_at: String,
|
||||
@@ -25,7 +35,7 @@ pub struct BenchResult {
|
||||
pub cargo_deps: HashMap<String, usize>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Represents a single line of parsed `strace` output.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct StraceOutput {
|
||||
pub percent_time: f64,
|
||||
@@ -35,6 +45,7 @@ pub struct StraceOutput {
|
||||
pub errors: u64,
|
||||
}
|
||||
|
||||
/// Get the compilation target triple for the current platform.
|
||||
pub fn get_target() -> &'static str {
|
||||
#[cfg(target_os = "macos")]
|
||||
return if cfg!(target_arch = "aarch64") {
|
||||
@@ -42,18 +53,22 @@ pub fn get_target() -> &'static str {
|
||||
} else {
|
||||
"x86_64-apple-darwin"
|
||||
};
|
||||
|
||||
#[cfg(target_os = "ios")]
|
||||
return if cfg!(target_arch = "aarch64") {
|
||||
"aarch64-apple-ios"
|
||||
} else {
|
||||
"x86_64-apple-ios"
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return "x86_64-unknown-linux-gnu";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
unimplemented!();
|
||||
unimplemented!("Windows target not implemented yet");
|
||||
}
|
||||
|
||||
/// Get the `target/release` directory path for benchmarks.
|
||||
pub fn target_dir() -> PathBuf {
|
||||
bench_root_path()
|
||||
.join("..")
|
||||
@@ -62,83 +77,90 @@ pub fn target_dir() -> PathBuf {
|
||||
.join("release")
|
||||
}
|
||||
|
||||
/// Get the root path of the current benchmark crate.
|
||||
pub fn bench_root_path() -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Get the home directory of the current user.
|
||||
pub fn home_path() -> PathBuf {
|
||||
#[cfg(any(target_os = "macos", target_os = "ios", target_os = "linux"))]
|
||||
return PathBuf::from(env!("HOME"));
|
||||
{
|
||||
PathBuf::from(std::env::var("HOME").unwrap_or_default())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return PathBuf::from(env!("HOMEPATH"));
|
||||
{
|
||||
PathBuf::from(std::env::var("USERPROFILE").unwrap_or_default())
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Get the root path of the Tauri repository.
|
||||
pub fn tauri_root_path() -> PathBuf {
|
||||
bench_root_path().parent().unwrap().to_path_buf()
|
||||
bench_root_path().parent().map(|p| p.to_path_buf()).unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn run_collect(cmd: &[&str]) -> (String, String) {
|
||||
let mut process_builder = Command::new(cmd[0]);
|
||||
process_builder
|
||||
/// Run a command and collect its stdout and stderr as strings.
|
||||
/// Returns an error if the command fails or exits with a non-zero status.
|
||||
pub fn run_collect(cmd: &[&str]) -> Result<(String, String)> {
|
||||
let output: Output = Command::new(cmd[0])
|
||||
.args(&cmd[1..])
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let prog = process_builder.spawn().expect("failed to spawn script");
|
||||
let Output {
|
||||
stdout,
|
||||
stderr,
|
||||
status,
|
||||
} = prog.wait_with_output().expect("failed to wait on child");
|
||||
let stdout = String::from_utf8_lossy(&stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&stderr).to_string();
|
||||
if !status.success() {
|
||||
eprintln!("stdout: <<<{stdout}>>>");
|
||||
eprintln!("stderr: <<<{stderr}>>>");
|
||||
panic!("Unexpected exit code: {:?}", status.code());
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.with_context(|| format!("failed to execute command: {cmd:?}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Command {:?} exited with {:?}\nstdout:\n{}\nstderr:\n{}",
|
||||
cmd,
|
||||
output.status.code(),
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
(stdout, stderr)
|
||||
|
||||
Ok((
|
||||
String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn parse_max_mem(file_path: &str) -> Option<u64> {
|
||||
let file = fs::File::open(file_path).unwrap();
|
||||
/// Parse a memory profiler (`mprof`) output file and return the maximum
|
||||
/// memory usage in bytes. Returns `None` if no values are found.
|
||||
pub fn parse_max_mem(file_path: &str) -> Result<Option<u64>> {
|
||||
let file = fs::File::open(file_path)
|
||||
.with_context(|| format!("failed to open mprof output file {file_path}"))?;
|
||||
let output = BufReader::new(file);
|
||||
|
||||
let mut highest: u64 = 0;
|
||||
// MEM 203.437500 1621617192.4123
|
||||
|
||||
for line in output.lines().map_while(Result::ok) {
|
||||
// split line by space
|
||||
let split = line.split(' ').collect::<Vec<_>>();
|
||||
let split: Vec<&str> = line.split(' ').collect();
|
||||
if split.len() == 3 {
|
||||
// mprof generate result in MB
|
||||
let current_bytes = str::parse::<f64>(split[1]).unwrap() as u64 * 1024 * 1024;
|
||||
if current_bytes > highest {
|
||||
highest = current_bytes;
|
||||
if let Ok(mb) = split[1].parse::<f64>() {
|
||||
let current_bytes = (mb * 1024.0 * 1024.0) as u64;
|
||||
highest = highest.max(current_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_file(file_path).unwrap();
|
||||
// Best-effort cleanup
|
||||
let _ = fs::remove_file(file_path);
|
||||
|
||||
if highest > 0 {
|
||||
return Some(highest);
|
||||
}
|
||||
|
||||
None
|
||||
Ok(if highest > 0 { Some(highest) } else { None })
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Parse the output of `strace -c` and return a summary of syscalls.
|
||||
pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
|
||||
let mut summary = HashMap::new();
|
||||
|
||||
let mut lines = output
|
||||
.lines()
|
||||
.filter(|line| !line.is_empty() && !line.contains("detached ..."));
|
||||
let count = lines.clone().count();
|
||||
|
||||
let count = lines.clone().count();
|
||||
if count < 4 {
|
||||
return summary;
|
||||
}
|
||||
@@ -148,88 +170,90 @@ pub fn parse_strace_output(output: &str) -> HashMap<String, StraceOutput> {
|
||||
let data_lines = lines.skip(2);
|
||||
|
||||
for line in data_lines {
|
||||
let syscall_fields = line.split_whitespace().collect::<Vec<_>>();
|
||||
let syscall_fields: Vec<&str> = line.split_whitespace().collect();
|
||||
let len = syscall_fields.len();
|
||||
let syscall_name = syscall_fields.last().unwrap();
|
||||
|
||||
if (5..=6).contains(&len) {
|
||||
summary.insert(
|
||||
syscall_name.to_string(),
|
||||
StraceOutput {
|
||||
percent_time: str::parse::<f64>(syscall_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(syscall_fields[1]).unwrap(),
|
||||
usecs_per_call: Some(str::parse::<u64>(syscall_fields[2]).unwrap()),
|
||||
calls: str::parse::<u64>(syscall_fields[3]).unwrap(),
|
||||
errors: if syscall_fields.len() < 6 {
|
||||
if let Some(&syscall_name) = syscall_fields.last() {
|
||||
if (5..=6).contains(&len) {
|
||||
let output = StraceOutput {
|
||||
percent_time: syscall_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: syscall_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: syscall_fields[2].parse().ok(),
|
||||
calls: syscall_fields[3].parse().unwrap_or(0),
|
||||
errors: if len < 6 {
|
||||
0
|
||||
} else {
|
||||
str::parse::<u64>(syscall_fields[4]).unwrap()
|
||||
syscall_fields[4].parse().unwrap_or(0)
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
summary.insert(syscall_name.to_string(), output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let total_fields = total_line.split_whitespace().collect::<Vec<_>>();
|
||||
|
||||
summary.insert(
|
||||
"total".to_string(),
|
||||
match total_fields.len() {
|
||||
// Old format, has no usecs/call
|
||||
5 => StraceOutput {
|
||||
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
|
||||
usecs_per_call: None,
|
||||
calls: str::parse::<u64>(total_fields[2]).unwrap(),
|
||||
errors: str::parse::<u64>(total_fields[3]).unwrap(),
|
||||
},
|
||||
6 => StraceOutput {
|
||||
percent_time: str::parse::<f64>(total_fields[0]).unwrap(),
|
||||
seconds: str::parse::<f64>(total_fields[1]).unwrap(),
|
||||
usecs_per_call: Some(str::parse::<u64>(total_fields[2]).unwrap()),
|
||||
calls: str::parse::<u64>(total_fields[3]).unwrap(),
|
||||
errors: str::parse::<u64>(total_fields[4]).unwrap(),
|
||||
},
|
||||
_ => panic!("Unexpected total field count: {}", total_fields.len()),
|
||||
let total_fields: Vec<&str> = total_line.split_whitespace().collect();
|
||||
let total = match total_fields.len() {
|
||||
5 => StraceOutput {
|
||||
percent_time: total_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: total_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: None,
|
||||
calls: total_fields[2].parse().unwrap_or(0),
|
||||
errors: total_fields[3].parse().unwrap_or(0),
|
||||
},
|
||||
);
|
||||
6 => StraceOutput {
|
||||
percent_time: total_fields[0].parse().unwrap_or(0.0),
|
||||
seconds: total_fields[1].parse().unwrap_or(0.0),
|
||||
usecs_per_call: total_fields[2].parse().ok(),
|
||||
calls: total_fields[3].parse().unwrap_or(0),
|
||||
errors: total_fields[4].parse().unwrap_or(0),
|
||||
},
|
||||
_ => {
|
||||
panic!("Unexpected total field count: {}", total_fields.len());
|
||||
}
|
||||
};
|
||||
|
||||
summary.insert("total".to_string(), total);
|
||||
summary
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn run(cmd: &[&str]) {
|
||||
let mut process_builder = Command::new(cmd[0]);
|
||||
process_builder.args(&cmd[1..]).stdin(Stdio::piped());
|
||||
let mut prog = process_builder.spawn().expect("failed to spawn script");
|
||||
let status = prog.wait().expect("failed to wait on child");
|
||||
/// Run a command and wait for completion.
|
||||
/// Returns an error if the command fails.
|
||||
pub fn run(cmd: &[&str]) -> Result<()> {
|
||||
let status = Command::new(cmd[0])
|
||||
.args(&cmd[1..])
|
||||
.stdin(Stdio::piped())
|
||||
.status()
|
||||
.with_context(|| format!("failed to execute command: {cmd:?}"))?;
|
||||
|
||||
if !status.success() {
|
||||
panic!("Unexpected exit code: {:?}", status.code());
|
||||
bail!("Command {:?} exited with {:?}", cmd, status.code());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Read a JSON file into a [`serde_json::Value`].
|
||||
pub fn read_json(filename: &str) -> Result<Value> {
|
||||
let f = fs::File::open(filename)?;
|
||||
let f =
|
||||
fs::File::open(filename).with_context(|| format!("failed to open JSON file {filename}"))?;
|
||||
Ok(serde_json::from_reader(f)?)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Write a [`serde_json::Value`] into a JSON file.
|
||||
pub fn write_json(filename: &str, value: &Value) -> Result<()> {
|
||||
let f = fs::File::create(filename)?;
|
||||
let f =
|
||||
fs::File::create(filename).with_context(|| format!("failed to create JSON file {filename}"))?;
|
||||
serde_json::to_writer(f, value)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn download_file(url: &str, filename: PathBuf) {
|
||||
/// Download a file from either a local path or an HTTP/HTTPS URL.
|
||||
/// Falls back to copying the file if the URL does not start with http/https.
|
||||
pub fn download_file(url: &str, filename: PathBuf) -> Result<()> {
|
||||
if !url.starts_with("http:") && !url.starts_with("https:") {
|
||||
fs::copy(url, filename).unwrap();
|
||||
return;
|
||||
fs::copy(url, &filename).with_context(|| format!("failed to copy from {url}"))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Downloading with curl this saves us from adding
|
||||
// a Rust HTTP client dependency.
|
||||
println!("Downloading {url}");
|
||||
let status = Command::new("curl")
|
||||
.arg("-L")
|
||||
@@ -238,8 +262,14 @@ pub fn download_file(url: &str, filename: PathBuf) {
|
||||
.arg(&filename)
|
||||
.arg(url)
|
||||
.status()
|
||||
.unwrap();
|
||||
.with_context(|| format!("failed to execute curl for {url}"))?;
|
||||
|
||||
assert!(status.success());
|
||||
assert!(filename.exists());
|
||||
if !status.success() {
|
||||
bail!("curl failed with exit code {:?}", status.code());
|
||||
}
|
||||
if !filename.exists() {
|
||||
bail!("expected file {:?} to exist after download", filename);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.5.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-codegen@2.5.1`
|
||||
|
||||
## \[2.5.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`4b6b8690a`](https://www.github.com/tauri-apps/tauri/commit/4b6b8690ab886ebdf1307951cffbe03e31280baa) ([#14347](https://www.github.com/tauri-apps/tauri/pull/14347) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused docs.rs builds to fail. No user facing changes.
|
||||
|
||||
## \[2.5.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`3b4fac201`](https://www.github.com/tauri-apps/tauri/commit/3b4fac2017832d426dd07c5e24e26684eda57f7b) ([#14194](https://www.github.com/tauri-apps/tauri/pull/14194)) Add `tauri.conf.json > bundle > android > autoIncrementVersionCode` config option to automatically increment the Android version code.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.0`
|
||||
- Upgraded to `tauri-codegen@2.5.0`
|
||||
|
||||
## \[2.4.1]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`c23bec62d`](https://www.github.com/tauri-apps/tauri/commit/c23bec62d6d5724798869681aa1534423aae28e2) ([#14083](https://www.github.com/tauri-apps/tauri/pull/14083) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Tauri now ignores `macOS.minimumSystemVersion` in `tauri dev` to prevent forced rebuilds of macOS specific dependencies when using something like `rust-analyzer` at the same time as `tauri dev`.
|
||||
|
||||
## \[2.4.0]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.7.0`
|
||||
- Upgraded to `tauri-codegen@2.4.0`
|
||||
|
||||
## \[2.3.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-build"
|
||||
version = "2.3.1"
|
||||
version = "2.5.2"
|
||||
description = "build time code to pair with https://crates.io/crates/tauri"
|
||||
exclude = ["CHANGELOG.md", "/target"]
|
||||
readme = "README.md"
|
||||
@@ -22,14 +22,12 @@ targets = [
|
||||
"x86_64-linux-android",
|
||||
"x86_64-apple-ios",
|
||||
]
|
||||
rustc-args = ["--cfg", "docsrs"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
quote = { version = "1", optional = true }
|
||||
tauri-codegen = { version = "2.3.1", path = "../tauri-codegen", optional = true }
|
||||
tauri-utils = { version = "2.6.0", path = "../tauri-utils", features = [
|
||||
tauri-codegen = { version = "2.5.1", path = "../tauri-codegen", optional = true }
|
||||
tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [
|
||||
"build",
|
||||
"resources",
|
||||
] }
|
||||
@@ -43,7 +41,7 @@ tauri-winres = "0.3"
|
||||
semver = "1"
|
||||
dirs = "6"
|
||||
glob = "0.3"
|
||||
toml = "0.8"
|
||||
toml = "0.9"
|
||||
# Our code requires at least 0.8.21 so don't simplify this to 0.8
|
||||
schemars = { version = "0.8.21", features = ["preserve_order"] }
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ fn read_plugins_manifests() -> Result<BTreeMap<String, Manifest>> {
|
||||
Ok(manifests)
|
||||
}
|
||||
|
||||
struct InlinedPuginsAcl {
|
||||
struct InlinedPluginsAcl {
|
||||
manifests: BTreeMap<String, Manifest>,
|
||||
permission_files: BTreeMap<String, Vec<PermissionFile>>,
|
||||
}
|
||||
@@ -165,7 +165,7 @@ struct InlinedPuginsAcl {
|
||||
fn inline_plugins(
|
||||
out_dir: &Path,
|
||||
inlined_plugins: HashMap<&'static str, InlinedPlugin>,
|
||||
) -> Result<InlinedPuginsAcl> {
|
||||
) -> Result<InlinedPluginsAcl> {
|
||||
let mut acl_manifests = BTreeMap::new();
|
||||
let mut permission_files_map = BTreeMap::new();
|
||||
|
||||
@@ -250,7 +250,7 @@ permissions = [{default_permissions}]
|
||||
acl_manifests.insert(name.into(), manifest);
|
||||
}
|
||||
|
||||
Ok(InlinedPuginsAcl {
|
||||
Ok(InlinedPluginsAcl {
|
||||
manifests: acl_manifests,
|
||||
permission_files: permission_files_map,
|
||||
})
|
||||
|
||||
@@ -120,6 +120,13 @@ impl CodegenContext {
|
||||
if info_plist_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", info_plist_path.display());
|
||||
}
|
||||
|
||||
if let Some(plist_path) = &config.bundle.macos.info_plist {
|
||||
let info_plist_path = config_parent.join(plist_path);
|
||||
if info_plist_path.exists() {
|
||||
println!("cargo:rerun-if-changed={}", info_plist_path.display());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let code = context_codegen(ContextData {
|
||||
|
||||
@@ -263,7 +263,7 @@ impl WindowsAttributes {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates the default attriute set wihtou the default app manifest.
|
||||
/// Creates the default attribute set without the default app manifest.
|
||||
#[must_use]
|
||||
pub fn new_without_app_manifest() -> Self {
|
||||
Self {
|
||||
@@ -499,7 +499,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
println!("cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX={android_package_prefix}");
|
||||
|
||||
if let Some(project_dir) = env::var_os("TAURI_ANDROID_PROJECT_PATH").map(PathBuf::from) {
|
||||
mobile::generate_gradle_files(project_dir, &config)?;
|
||||
mobile::generate_gradle_files(project_dir)?;
|
||||
}
|
||||
|
||||
cfg_alias("dev", is_dev());
|
||||
@@ -573,8 +573,10 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(version) = &config.bundle.macos.minimum_system_version {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
|
||||
if !is_dev() {
|
||||
if let Some(version) = &config.bundle.macos.minimum_system_version {
|
||||
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET={version}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{fs::write, path::PathBuf};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use semver::Version;
|
||||
use tauri_utils::{config::Config, write_if_changed};
|
||||
use tauri_utils::write_if_changed;
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
pub fn generate_gradle_files(project_dir: PathBuf, config: &Config) -> Result<()> {
|
||||
pub fn generate_gradle_files(project_dir: PathBuf) -> Result<()> {
|
||||
let gradle_settings_path = project_dir.join("tauri.settings.gradle");
|
||||
let app_build_gradle_path = project_dir.join("app").join("tauri.build.gradle.kts");
|
||||
let app_tauri_properties_path = project_dir.join("app").join("tauri.properties");
|
||||
|
||||
let mut gradle_settings =
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n".to_string();
|
||||
@@ -21,7 +17,6 @@ pub fn generate_gradle_files(project_dir: PathBuf, config: &Config) -> Result<()
|
||||
val implementation by configurations
|
||||
dependencies {"
|
||||
.to_string();
|
||||
let mut app_tauri_properties = Vec::new();
|
||||
|
||||
for (env, value) in std::env::vars_os() {
|
||||
let env = env.to_string_lossy();
|
||||
@@ -54,32 +49,6 @@ dependencies {"
|
||||
|
||||
app_build_gradle.push_str("\n}");
|
||||
|
||||
if let Some(version) = config.version.as_ref() {
|
||||
app_tauri_properties.push(format!("tauri.android.versionName={version}"));
|
||||
if let Some(version_code) = config.bundle.android.version_code.as_ref() {
|
||||
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
|
||||
} else if let Ok(version) = Version::parse(version) {
|
||||
let mut version_code = version.major * 1000000 + version.minor * 1000 + version.patch;
|
||||
|
||||
if is_dev() {
|
||||
version_code = version_code.clamp(1, 2100000000);
|
||||
}
|
||||
|
||||
if version_code == 0 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"You must change the `version` in `tauri.conf.json`. The default value `0.0.0` is not allowed for Android package and must be at least `0.0.1`."
|
||||
));
|
||||
} else if version_code > 2100000000 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Invalid version code {}. Version code must be between 1 and 2100000000. You must change the `version` in `tauri.conf.json`.",
|
||||
version_code
|
||||
));
|
||||
}
|
||||
|
||||
app_tauri_properties.push(format!("tauri.android.versionCode={version_code}"));
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite only if changed to not trigger rebuilds
|
||||
write_if_changed(&gradle_settings_path, gradle_settings)
|
||||
.context("failed to write tauri.settings.gradle")?;
|
||||
@@ -87,28 +56,8 @@ dependencies {"
|
||||
write_if_changed(&app_build_gradle_path, app_build_gradle)
|
||||
.context("failed to write tauri.build.gradle.kts")?;
|
||||
|
||||
if !app_tauri_properties.is_empty() {
|
||||
let app_tauri_properties_content = format!(
|
||||
"// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.\n{}",
|
||||
app_tauri_properties.join("\n")
|
||||
);
|
||||
if std::fs::read_to_string(&app_tauri_properties_path)
|
||||
.map(|o| o != app_tauri_properties_content)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
write(&app_tauri_properties_path, app_tauri_properties_content)
|
||||
.context("failed to write tauri.properties")?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("cargo:rerun-if-changed={}", gradle_settings_path.display());
|
||||
println!("cargo:rerun-if-changed={}", app_build_gradle_path.display());
|
||||
if !app_tauri_properties.is_empty() {
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
app_tauri_properties_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,84 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.7.3]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`22edc65aa`](https://www.github.com/tauri-apps/tauri/commit/22edc65aad0b3e45515008e8e0866112da70c8a1) ([#14408](https://www.github.com/tauri-apps/tauri/pull/14408) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Set user-agent in bundler and cli http requests when fetching build tools.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`9a1922636`](https://www.github.com/tauri-apps/tauri/commit/9a192263693d71123a9953e2a6ee60fad07500b4) ([#14410](https://www.github.com/tauri-apps/tauri/pull/14410) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix uninstall fails if you close the app manually during the 'Click Ok to kill it' dialog
|
||||
|
||||
## \[2.7.2]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`7f710b8f3`](https://www.github.com/tauri-apps/tauri/commit/7f710b8f3b509ed327d76761926511cf56e66b2d) ([#14390](https://www.github.com/tauri-apps/tauri/pull/14390) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Inline linuxdeploy plugins which were previously downloaded from `https://raw.githubusercontent.com` which lately blocks many users with a 429 error.
|
||||
- [`fc017ee25`](https://www.github.com/tauri-apps/tauri/commit/fc017ee2577f48615367ea519386d3f37837e2c1) ([#14368](https://www.github.com/tauri-apps/tauri/pull/14368) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Mention symbol stripping on Linux in binary patch failed warning message
|
||||
|
||||
## \[2.7.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-macos-sign@2.3.0`
|
||||
|
||||
## \[2.7.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`2a06d1006`](https://www.github.com/tauri-apps/tauri/commit/2a06d10066a806e392efe8bfb16d943ee0b0b61d) ([#14052](https://www.github.com/tauri-apps/tauri/pull/14052)) Add a `--no-sign` flag to the `tauri build` and `tauri bundle` commands to skip the code signing step, improving the developer experience for local testing and development without requiring code signing keys.
|
||||
- [`cc8c0b531`](https://www.github.com/tauri-apps/tauri/commit/cc8c0b53171173dbd1d01781a50de1a3ea159031) ([#14031](https://www.github.com/tauri-apps/tauri/pull/14031)) Support providing `plist::Value` as macOS entitlements.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`b06b3bd09`](https://www.github.com/tauri-apps/tauri/commit/b06b3bd091b0fed26cdcfb23cacb0462a7a9cc2d) ([#14126](https://www.github.com/tauri-apps/tauri/pull/14126)) Improve error messages with more context.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`06d4a4ed6`](https://www.github.com/tauri-apps/tauri/commit/06d4a4ed6c146d6c7782016cf90037b56b944445) ([#14241](https://www.github.com/tauri-apps/tauri/pull/14241)) Set `APPIMAGE_EXTRACT_AND_RUN` on top of using the `--appimage-extra-and-run` cli arg for linuxdeploy.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.0`
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- [`ed7c9a410`](https://www.github.com/tauri-apps/tauri/commit/ed7c9a4100e08c002212265549d12130d021ad1e) ([#14108](https://www.github.com/tauri-apps/tauri/pull/14108)) Changed `MacOsSettings::info_plist_path` to `MacOsSettings::info_plist`.
|
||||
|
||||
## \[2.6.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`f3df96fb3`](https://www.github.com/tauri-apps/tauri/commit/f3df96fb38e2f27ce6bf232fe87f35bcfec50ce4) ([#14065](https://www.github.com/tauri-apps/tauri/pull/14065) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix binary patching updater type fails on 32 bit Windows builds
|
||||
|
||||
## \[2.6.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`a9ec12843`](https://www.github.com/tauri-apps/tauri/commit/a9ec12843aa7d0eb774bd3a53e2e63da12cfa77b) ([#13521](https://www.github.com/tauri-apps/tauri/pull/13521) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added a `--skip-stapling` option to make `tauri build|bundle` *not* wait for notarization to finish on macOS.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`8b465a12b`](https://www.github.com/tauri-apps/tauri/commit/8b465a12ba73e94d7a3995defd9cc362d15eeebe) ([#13913](https://www.github.com/tauri-apps/tauri/pull/13913) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler now pulls the latest AppImage linuxdeploy plugin instead of using the built-in one. This should remove the libfuse requirement.
|
||||
- [`4475e93e1`](https://www.github.com/tauri-apps/tauri/commit/4475e93e136e9e2bd5f3c7817fa2040924f630f6) ([#13824](https://www.github.com/tauri-apps/tauri/pull/13824) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler and cli will now read TLS Certificates installed on the system when downloading tools and checking versions.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`a8f1569b0`](https://www.github.com/tauri-apps/tauri/commit/a8f1569b04edf7b54a19e19ad37b421b0808f512) ([#13921](https://www.github.com/tauri-apps/tauri/pull/13921) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) The bundler will no longer try to sign non-binary and already signed binary files on Windows
|
||||
- [`bc6b125b2`](https://www.github.com/tauri-apps/tauri/commit/bc6b125b24589ffc412a4f17d899a387a0fc0bb2) ([#13909](https://www.github.com/tauri-apps/tauri/pull/13909) by [@Andrew15-5](https://www.github.com/tauri-apps/tauri/../../Andrew15-5)) The bundler now falls back to `1` for the release in case an empty string was provided instead of using `-.` in the file name.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.7.0`
|
||||
- Upgraded to `tauri-macos-sign@2.2.0`
|
||||
|
||||
## \[2.5.2]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`af95fb601`](https://www.github.com/tauri-apps/tauri/commit/af95fb6014ea54a2636bfd299095608f6cd93221) ([#13870](https://www.github.com/tauri-apps/tauri/pull/13870) by [@kittuov](https://www.github.com/tauri-apps/tauri/../../kittuov)) The bundler now signs the main binary after patching it for every package type on windows
|
||||
|
||||
## \[2.5.1]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-bundler"
|
||||
version = "2.5.1"
|
||||
version = "2.7.3"
|
||||
authors = [
|
||||
"George Burton <burtonageo@gmail.com>",
|
||||
"Tauri Programme within The Commons Conservancy",
|
||||
@@ -15,13 +15,13 @@ rust-version = "1.77.2"
|
||||
exclude = ["CHANGELOG.md", "/target", "rustfmt.toml"]
|
||||
|
||||
[dependencies]
|
||||
tauri-utils = { version = "2.6.0", path = "../tauri-utils", features = [
|
||||
tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [
|
||||
"resources",
|
||||
] }
|
||||
image = "0.25"
|
||||
flate2 = "1"
|
||||
anyhow = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strsim = "0.11"
|
||||
@@ -44,6 +44,8 @@ url = "2"
|
||||
uuid = { version = "1", features = ["v4", "v5"] }
|
||||
regex = "1"
|
||||
goblin = "0.9"
|
||||
plist = "1"
|
||||
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
bitness = "0.4"
|
||||
@@ -57,8 +59,7 @@ features = ["Win32_System_SystemInformation", "Win32_System_Diagnostics_Debug"]
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
icns = { package = "tauri-icns", version = "0.1" }
|
||||
time = { version = "0.3", features = ["formatting"] }
|
||||
plist = "1"
|
||||
tauri-macos-sign = { version = "2.1.0", path = "../tauri-macos-sign" }
|
||||
tauri-macos-sign = { version = "2.3.0", path = "../tauri-macos-sign" }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
heck = "0.5"
|
||||
@@ -67,14 +68,15 @@ md5 = "0.8"
|
||||
rpm = { version = "0.16", features = ["bzip2-compression"] }
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
which = "7"
|
||||
which = "8"
|
||||
|
||||
[lib]
|
||||
name = "tauri_bundler"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
default = ["rustls", "platform-certs"]
|
||||
native-tls = ["ureq/native-tls"]
|
||||
native-tls-vendored = ["native-tls", "native-tls/vendored"]
|
||||
rustls = ["ureq/rustls"]
|
||||
platform-certs = ["ureq/platform-verifier"]
|
||||
|
||||
@@ -45,15 +45,17 @@ pub use self::{
|
||||
category::AppCategory,
|
||||
settings::{
|
||||
AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings,
|
||||
DmgSettings, IosSettings, MacOsSettings, PackageSettings, PackageType, Position, RpmSettings,
|
||||
Settings, SettingsBuilder, Size, UpdaterSettings,
|
||||
DmgSettings, Entitlements, IosSettings, MacOsSettings, PackageSettings, PackageType, PlistKind,
|
||||
Position, RpmSettings, Settings, SettingsBuilder, Size, UpdaterSettings,
|
||||
},
|
||||
};
|
||||
#[cfg(target_os = "macos")]
|
||||
use anyhow::Context;
|
||||
pub use settings::{NsisSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings};
|
||||
|
||||
use std::{fmt::Write, path::PathBuf};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
io::{Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
/// Generated bundle metadata.
|
||||
#[derive(Debug)]
|
||||
@@ -81,44 +83,31 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
}
|
||||
|
||||
// Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
if settings.can_sign() {
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
windows::sign::try_sign(&bin_path, settings)?;
|
||||
}
|
||||
|
||||
// Sign the sidecar binaries
|
||||
for bin in settings.external_binaries() {
|
||||
let path = bin?;
|
||||
let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").is_ok_and(|v| v == "true");
|
||||
if skip {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if windows::sign::verify(&path)? {
|
||||
log::info!(
|
||||
"sidecar at \"{}\" already signed. Skipping...",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
windows::sign::try_sign(&path, settings)?;
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
|
||||
}
|
||||
}
|
||||
sign_binaries_if_needed(settings, target_os)?;
|
||||
|
||||
let main_binary = settings
|
||||
.binaries()
|
||||
.iter()
|
||||
.find(|b| b.main())
|
||||
.expect("Main binary missing in settings");
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
|
||||
// When packaging multiple binary types, we make a copy of the unsigned main_binary so that we can
|
||||
// restore it after each package_type step. This avoids two issues:
|
||||
// - modifying a signed binary without updating its PE checksum can break signature verification
|
||||
// - codesigning tools should handle calculating+updating this, we just need to ensure
|
||||
// (re)signing is performed after every `patch_binary()` operation
|
||||
// - signing an already-signed binary can result in multiple signatures, causing verification errors
|
||||
let main_binary_reset_required = matches!(target_os, TargetPlatform::Windows)
|
||||
&& settings.windows().can_sign()
|
||||
&& package_types.len() > 1;
|
||||
let mut unsigned_main_binary_copy = tempfile::tempfile()?;
|
||||
if main_binary_reset_required {
|
||||
let mut unsigned_main_binary = std::fs::File::open(&main_binary_path)?;
|
||||
std::io::copy(&mut unsigned_main_binary, &mut unsigned_main_binary_copy)?;
|
||||
}
|
||||
|
||||
let mut main_binary_signed = false;
|
||||
let mut bundles = Vec::<Bundle>::new();
|
||||
for package_type in &package_types {
|
||||
// bundle was already built! e.g. DMG already built .app
|
||||
@@ -126,10 +115,24 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = patch_binary(&settings.binary_path(main_binary), package_type) {
|
||||
if let Err(e) = patch_binary(&main_binary_path, package_type) {
|
||||
log::warn!("Failed to add bundler type to the binary: {e}. Updater plugin may not be able to update this package. This shouldn't normally happen, please report it to https://github.com/tauri-apps/tauri/issues");
|
||||
}
|
||||
|
||||
// sign main binary for every package type after patch
|
||||
if matches!(target_os, TargetPlatform::Windows) && settings.windows().can_sign() {
|
||||
if main_binary_signed && main_binary_reset_required {
|
||||
let mut signed_main_binary = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(&main_binary_path)?;
|
||||
unsigned_main_binary_copy.seek(SeekFrom::Start(0))?;
|
||||
std::io::copy(&mut unsigned_main_binary_copy, &mut signed_main_binary)?;
|
||||
}
|
||||
windows::sign::try_sign(&main_binary_path, settings)?;
|
||||
main_binary_signed = true;
|
||||
}
|
||||
|
||||
let bundle_paths = match package_type {
|
||||
#[cfg(target_os = "macos")]
|
||||
PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
|
||||
@@ -150,6 +153,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
|
||||
// note: don't restrict to windows as NSIS installers can be built in linux using cargo-xwin
|
||||
PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -217,31 +221,30 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
.map(|b| b.bundle_paths)
|
||||
{
|
||||
for app_bundle_path in &app_bundle_paths {
|
||||
use crate::error::ErrorExt;
|
||||
|
||||
log::info!(action = "Cleaning"; "{}", app_bundle_path.display());
|
||||
match app_bundle_path.is_dir() {
|
||||
true => std::fs::remove_dir_all(app_bundle_path),
|
||||
false => std::fs::remove_file(app_bundle_path),
|
||||
}
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Failed to clean the app bundle at {}",
|
||||
app_bundle_path.display()
|
||||
)
|
||||
})?
|
||||
.fs_context(
|
||||
"failed to clean the app bundle",
|
||||
app_bundle_path.to_path_buf(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bundles.is_empty() {
|
||||
return Err(anyhow::anyhow!("No bundles were built").into());
|
||||
return Ok(bundles);
|
||||
}
|
||||
|
||||
let bundles_wo_updater = bundles
|
||||
let finished_bundles = bundles
|
||||
.iter()
|
||||
.filter(|b| b.package_type != PackageType::Updater)
|
||||
.collect::<Vec<_>>();
|
||||
let finished_bundles = bundles_wo_updater.len();
|
||||
.count();
|
||||
let pluralised = if finished_bundles == 1 {
|
||||
"bundle"
|
||||
} else {
|
||||
@@ -266,6 +269,51 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
fn sign_binaries_if_needed(settings: &Settings, target_os: &TargetPlatform) -> crate::Result<()> {
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
if settings.windows().can_sign() {
|
||||
if settings.no_sign() {
|
||||
log::info!("Skipping binary signing due to --no-sign flag.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for bin in settings.binaries() {
|
||||
if bin.main() {
|
||||
// we will sign the main binary after patching per "package type"
|
||||
continue;
|
||||
}
|
||||
let bin_path = settings.binary_path(bin);
|
||||
windows::sign::try_sign(&bin_path, settings)?;
|
||||
}
|
||||
|
||||
// Sign the sidecar binaries
|
||||
for bin in settings.external_binaries() {
|
||||
let path = bin?;
|
||||
let skip = std::env::var("TAURI_SKIP_SIDECAR_SIGNATURE_CHECK").is_ok_and(|v| v == "true");
|
||||
if skip {
|
||||
continue;
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
if windows::sign::verify(&path)? {
|
||||
log::info!(
|
||||
"sidecar at \"{}\" already signed. Skipping...",
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
windows::sign::try_sign(&path, settings)?;
|
||||
}
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
log::warn!("Signing, by default, is only supported on Windows hosts, but you can specify a custom signing command in `bundler > windows > sign_command`, for now, skipping signing the installer...");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check to see if there are icons in the settings struct
|
||||
pub fn check_icons(settings: &Settings) -> crate::Result<bool> {
|
||||
// make a peekable iterator of the icon_files
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
#! /bin/bash
|
||||
|
||||
# abort on all errors
|
||||
set -e
|
||||
|
||||
if [ "$DEBUG" != "" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
script=$(readlink -f "$0")
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $script --appdir <path to AppDir>"
|
||||
echo
|
||||
echo "Bundles GStreamer plugins into an AppDir"
|
||||
echo
|
||||
echo "Required variables:"
|
||||
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
|
||||
echo
|
||||
echo "Optional variables:"
|
||||
echo " GSTREAMER_INCLUDE_BAD_PLUGINS=\"1\" (default: disabled; set to empty string or unset to disable)"
|
||||
echo " GSTREAMER_PLUGINS_DIR=\"...\" (directory containing GStreamer plugins; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_HELPERS_DIR=\"...\" (directory containing GStreamer helper tools like gst-plugin-scanner; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_VERSION=\"1.0\" (default: 1.0)"
|
||||
}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
--plugin-api-version)
|
||||
echo "0"
|
||||
exit 0
|
||||
;;
|
||||
--appdir)
|
||||
APPDIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument: $1"
|
||||
echo
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$APPDIR" == "" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! which patchelf &>/dev/null && ! type patchelf &>/dev/null; then
|
||||
echo "Error: patchelf not found"
|
||||
echo
|
||||
show_usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$LINUXDEPLOY" == "" ]]; then
|
||||
echo "Error: \$LINUXDEPLOY not set"
|
||||
echo
|
||||
show_usage
|
||||
exit 3
|
||||
fi
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
|
||||
export GSTREAMER_VERSION="${GSTREAMER_VERSION:-1.0}"
|
||||
|
||||
plugins_target_dir="$APPDIR"/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
helpers_target_dir="$APPDIR"/usr/lib/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
|
||||
if [ "$GSTREAMER_PLUGINS_DIR" != "" ]; then
|
||||
plugins_dir="${GSTREAMER_PLUGINS_DIR}"
|
||||
elif [ -d /usr/lib/"$(uname -m)"-linux-gnu/gstreamer-"$GSTREAMER_VERSION" ]; then
|
||||
plugins_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer-"$GSTREAMER_VERSION"
|
||||
else
|
||||
plugins_dir=/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ "$GSTREAMER_HELPERS_DIR" != "" ]; then
|
||||
helpers_dir="${GSTREAMER_HELPERS_DIR}"
|
||||
else
|
||||
helpers_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ ! -d "$plugins_dir" ]; then
|
||||
echo "Error: could not find plugins directory: $plugins_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$plugins_target_dir"
|
||||
|
||||
echo "Copying plugins into $plugins_target_dir"
|
||||
for i in "$plugins_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying plugin: $i"
|
||||
cp "$i" "$plugins_target_dir"
|
||||
done
|
||||
|
||||
"$LINUXDEPLOY" --appdir "$APPDIR"
|
||||
|
||||
for i in "$plugins_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/..:$ORIGIN' "$i"
|
||||
done
|
||||
|
||||
mkdir -p "$helpers_target_dir"
|
||||
|
||||
echo "Copying helpers in $helpers_target_dir"
|
||||
for i in "$helpers_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying helper: $i"
|
||||
cp "$i" "$helpers_target_dir"
|
||||
done
|
||||
|
||||
for i in "$helpers_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/../..' "$i"
|
||||
done
|
||||
|
||||
echo "Installing AppRun hook"
|
||||
mkdir -p "$APPDIR"/apprun-hooks
|
||||
|
||||
if [ "$GSTREAMER_VERSION" == "1.0" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
export GST_PLUGIN_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
elif [ "$GSTREAMER_VERSION" == "0.10" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_0_10="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
else
|
||||
echo "Warning: unknown GStreamer version: $GSTREAMER_VERSION, cannot install AppRun hook"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
# GTK3 environment variables: https://developer.gnome.org/gtk3/stable/gtk-running.html
|
||||
# GTK4 environment variables: https://developer.gnome.org/gtk4/stable/gtk-running.html
|
||||
|
||||
# abort on all errors
|
||||
set -e
|
||||
|
||||
if [ "$DEBUG" != "" ]; then
|
||||
set -x
|
||||
verbose="--verbose"
|
||||
fi
|
||||
|
||||
script=$(readlink -f "$0")
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $script --appdir <path to AppDir>"
|
||||
echo
|
||||
echo "Bundles resources for applications that use GTK into an AppDir"
|
||||
echo
|
||||
echo "Required variables:"
|
||||
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
|
||||
#echo
|
||||
#echo "Optional variables:"
|
||||
#echo " DEPLOY_GTK_VERSION (major version of GTK to deploy, e.g. '2', '3' or '4'; auto-detect by default)"
|
||||
}
|
||||
|
||||
variable_is_true() {
|
||||
local var="$1"
|
||||
|
||||
if [ -n "$var" ] && { [ "$var" == "true" ] || [ "$var" -gt 0 ]; } 2> /dev/null; then
|
||||
return 0 # true
|
||||
else
|
||||
return 1 # false
|
||||
fi
|
||||
}
|
||||
|
||||
get_pkgconf_variable() {
|
||||
local variable="$1"
|
||||
local library="$2"
|
||||
local default_path="$3"
|
||||
|
||||
path="$("$PKG_CONFIG" --variable="$variable" "$library")"
|
||||
if [ -n "$path" ]; then
|
||||
echo "$path"
|
||||
elif [ -n "$default_path" ]; then
|
||||
echo "$default_path"
|
||||
else
|
||||
echo "$0: there is no '$variable' variable for '$library' library." > /dev/stderr
|
||||
echo "Please check the '$library.pc' file is present in \$PKG_CONFIG_PATH (you may need to install the appropriate -dev/-devel package)." > /dev/stderr
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
copy_tree() {
|
||||
local src=("${@:1:$#-1}")
|
||||
local dst="${*:$#}"
|
||||
|
||||
for elem in "${src[@]}"; do
|
||||
mkdir -p "${dst::-1}$elem"
|
||||
cp "$elem" --archive --parents --target-directory="$dst" $verbose
|
||||
done
|
||||
}
|
||||
|
||||
search_tool() {
|
||||
local tool="$1"
|
||||
local directory="$2"
|
||||
|
||||
if command -v "$tool"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
PATH_ARRAY=(
|
||||
"/usr/lib/$(uname -m)-linux-gnu/$directory/$tool"
|
||||
"/usr/lib/$directory/$tool"
|
||||
"/usr/bin/$tool"
|
||||
"/usr/bin/$tool-64"
|
||||
"/usr/bin/$tool-32"
|
||||
)
|
||||
|
||||
for path in "${PATH_ARRAY[@]}"; do
|
||||
if [ -x "$path" ]; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
#DEPLOY_GTK_VERSION="${DEPLOY_GTK_VERSION:-0}" # When not set by user, this variable use the integer '0' as a sentinel value
|
||||
DEPLOY_GTK_VERSION=3 # Force GTK3 for tauri apps
|
||||
APPDIR=
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
--plugin-api-version)
|
||||
echo "0"
|
||||
exit 0
|
||||
;;
|
||||
--appdir)
|
||||
APPDIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument: $1"
|
||||
echo
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$APPDIR" == "" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
# make lib64 writable again.
|
||||
chmod +w "$APPDIR"/usr/lib64 || true
|
||||
|
||||
if command -v pkgconf > /dev/null; then
|
||||
PKG_CONFIG="pkgconf"
|
||||
elif command -v pkg-config > /dev/null; then
|
||||
PKG_CONFIG="pkg-config"
|
||||
else
|
||||
echo "$0: pkg-config/pkgconf not found in PATH, aborting"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v find &>/dev/null && ! type find &>/dev/null; then
|
||||
echo -e "$0: find not found.\nInstall findutils then re-run the plugin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$LINUXDEPLOY" ]; then
|
||||
echo -e "$0: LINUXDEPLOY environment variable is not set.\nDownload a suitable linuxdeploy AppImage, set the environment variable and re-run the plugin."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
gtk_versions=0 # Count major versions of GTK when auto-detect GTK version
|
||||
if [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
|
||||
echo "Determining which GTK version to deploy"
|
||||
while IFS= read -r -d '' file; do
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 2 ] && ldd "$file" | grep -q "libgtk-x11-2.0.so"; then
|
||||
DEPLOY_GTK_VERSION=2
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 3 ] && ldd "$file" | grep -q "libgtk-3.so"; then
|
||||
DEPLOY_GTK_VERSION=3
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
if [ "$DEPLOY_GTK_VERSION" -ne 4 ] && ldd "$file" | grep -q "libgtk-4.so"; then
|
||||
DEPLOY_GTK_VERSION=4
|
||||
gtk_versions="$((gtk_versions+1))"
|
||||
fi
|
||||
done < <(find "$APPDIR/usr/bin" -executable -type f -print0)
|
||||
fi
|
||||
|
||||
if [ "$gtk_versions" -gt 1 ]; then
|
||||
echo "$0: can not deploy multiple GTK versions at the same time."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
elif [ "$DEPLOY_GTK_VERSION" -eq 0 ]; then
|
||||
echo "$0: failed to auto-detect GTK version."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing AppRun hook"
|
||||
HOOKSDIR="$APPDIR/apprun-hooks"
|
||||
HOOKFILE="$HOOKSDIR/linuxdeploy-plugin-gtk.sh"
|
||||
mkdir -p "$HOOKSDIR"
|
||||
cat > "$HOOKFILE" <<\EOF
|
||||
#! /usr/bin/env bash
|
||||
|
||||
gsettings get org.gnome.desktop.interface gtk-theme 2> /dev/null | grep -qi "dark" && GTK_THEME_VARIANT="dark" || GTK_THEME_VARIANT="light"
|
||||
APPIMAGE_GTK_THEME="${APPIMAGE_GTK_THEME:-"Adwaita:$GTK_THEME_VARIANT"}" # Allow user to override theme (discouraged)
|
||||
|
||||
export APPDIR="${APPDIR:-"$(dirname "$(realpath "$0")")"}" # Workaround to run extracted AppImage
|
||||
export GTK_DATA_PREFIX="$APPDIR"
|
||||
export GTK_THEME="$APPIMAGE_GTK_THEME" # Custom themes are broken
|
||||
export GDK_BACKEND=x11 # Crash with Wayland backend on Wayland - We tested it without it and ended up with this: https://github.com/tauri-apps/tauri/issues/8541
|
||||
export XDG_DATA_DIRS="$APPDIR/usr/share:/usr/share:$XDG_DATA_DIRS" # g_get_system_data_dirs() from GLib
|
||||
EOF
|
||||
|
||||
echo "Installing GLib schemas"
|
||||
# Note: schemasdir is undefined on Ubuntu 16.04
|
||||
glib_schemasdir="$(get_pkgconf_variable "schemasdir" "gio-2.0" "/usr/share/glib-2.0/schemas")"
|
||||
copy_tree "$glib_schemasdir" "$APPDIR/"
|
||||
glib-compile-schemas "$APPDIR/$glib_schemasdir"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GSETTINGS_SCHEMA_DIR="\$APPDIR/$glib_schemasdir"
|
||||
EOF
|
||||
|
||||
case "$DEPLOY_GTK_VERSION" in
|
||||
2)
|
||||
# https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/pull/20#issuecomment-826354261
|
||||
echo "WARNING: Gtk+2 applications are not fully supported by this plugin"
|
||||
;;
|
||||
3)
|
||||
echo "Installing GTK 3.0 modules"
|
||||
gtk3_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk+-3.0")"
|
||||
gtk3_libdir="$(get_pkgconf_variable "libdir" "gtk+-3.0")/gtk-3.0"
|
||||
#gtk3_path="$gtk3_libdir/modules" export GTK_PATH="\$APPDIR/$gtk3_path"
|
||||
gtk3_immodulesdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0")/immodules"
|
||||
gtk3_printbackendsdir="$gtk3_libdir/$(get_pkgconf_variable "gtk_binary_version" "gtk+-3.0")/printbackends"
|
||||
gtk3_immodules_cache_file="$(dirname "$gtk3_immodulesdir")/immodules.cache"
|
||||
gtk3_immodules_query="$(search_tool "gtk-query-immodules-3.0" "libgtk-3-0")"
|
||||
copy_tree "$gtk3_libdir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GTK_EXE_PREFIX="\$APPDIR/$gtk3_exec_prefix"
|
||||
export GTK_PATH="\$APPDIR/$gtk3_libdir:/usr/lib64/gtk-3.0:/usr/lib/x86_64-linux-gnu/gtk-3.0"
|
||||
export GTK_IM_MODULE_FILE="\$APPDIR/$gtk3_immodules_cache_file"
|
||||
|
||||
EOF
|
||||
if [ -x "$gtk3_immodules_query" ]; then
|
||||
echo "Updating immodules cache in $APPDIR/$gtk3_immodules_cache_file"
|
||||
"$gtk3_immodules_query" > "$APPDIR/$gtk3_immodules_cache_file"
|
||||
else
|
||||
echo "WARNING: gtk-query-immodules-3.0 not found"
|
||||
fi
|
||||
if [ ! -f "$APPDIR/$gtk3_immodules_cache_file" ]; then
|
||||
echo "WARNING: immodules.cache file is missing"
|
||||
fi
|
||||
sed -i "s|$gtk3_libdir/3.0.0/immodules/||g" "$APPDIR/$gtk3_immodules_cache_file"
|
||||
;;
|
||||
4)
|
||||
echo "Installing GTK 4.0 modules"
|
||||
gtk4_exec_prefix="$(get_pkgconf_variable "exec_prefix" "gtk4" "/usr")"
|
||||
gtk4_libdir="$(get_pkgconf_variable "libdir" "gtk4")/gtk-4.0"
|
||||
gtk4_path="$gtk4_libdir/modules"
|
||||
copy_tree "$gtk4_libdir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GTK_EXE_PREFIX="\$APPDIR/$gtk4_exec_prefix"
|
||||
export GTK_PATH="\$APPDIR/$gtk4_path"
|
||||
EOF
|
||||
;;
|
||||
*)
|
||||
echo "$0: '$DEPLOY_GTK_VERSION' is not a valid GTK major version."
|
||||
echo "Please set DEPLOY_GTK_VERSION to {2, 3, 4}."
|
||||
exit 1
|
||||
esac
|
||||
|
||||
echo "Installing GDK PixBufs"
|
||||
gdk_libdir="$(get_pkgconf_variable "libdir" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_binarydir="$(get_pkgconf_variable "gdk_pixbuf_binarydir" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_cache_file="$(get_pkgconf_variable "gdk_pixbuf_cache_file" "gdk-pixbuf-2.0")"
|
||||
gdk_pixbuf_moduledir="$(get_pkgconf_variable "gdk_pixbuf_moduledir" "gdk-pixbuf-2.0")"
|
||||
# Note: gdk_pixbuf_query_loaders variable is not defined on some systems
|
||||
gdk_pixbuf_query="$(search_tool "gdk-pixbuf-query-loaders" "gdk-pixbuf-2.0")"
|
||||
copy_tree "$gdk_pixbuf_binarydir" "$APPDIR/"
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GDK_PIXBUF_MODULE_FILE="\$APPDIR/$gdk_pixbuf_cache_file"
|
||||
EOF
|
||||
if [ -x "$gdk_pixbuf_query" ]; then
|
||||
echo "Updating pixbuf cache in $APPDIR/$gdk_pixbuf_cache_file"
|
||||
"$gdk_pixbuf_query" > "$APPDIR/$gdk_pixbuf_cache_file"
|
||||
else
|
||||
echo "WARNING: gdk-pixbuf-query-loaders not found"
|
||||
fi
|
||||
if [ ! -f "$APPDIR/$gdk_pixbuf_cache_file" ]; then
|
||||
echo "WARNING: loaders.cache file is missing"
|
||||
fi
|
||||
sed -i "s|$gdk_pixbuf_moduledir/||g" "$APPDIR/$gdk_pixbuf_cache_file"
|
||||
|
||||
echo "Copying more libraries"
|
||||
gobject_libdir="$(get_pkgconf_variable "libdir" "gobject-2.0")"
|
||||
gio_libdir="$(get_pkgconf_variable "libdir" "gio-2.0")"
|
||||
librsvg_libdir="$(get_pkgconf_variable "libdir" "librsvg-2.0")"
|
||||
pango_libdir="$(get_pkgconf_variable "libdir" "pango")"
|
||||
pangocairo_libdir="$(get_pkgconf_variable "libdir" "pangocairo")"
|
||||
pangoft2_libdir="$(get_pkgconf_variable "libdir" "pangoft2")"
|
||||
FIND_ARRAY=(
|
||||
"$gdk_libdir" "libgdk_pixbuf-*.so*"
|
||||
"$gobject_libdir" "libgobject-*.so*"
|
||||
"$gio_libdir" "libgio-*.so*"
|
||||
"$librsvg_libdir" "librsvg-*.so*"
|
||||
"$pango_libdir" "libpango-*.so*"
|
||||
"$pangocairo_libdir" "libpangocairo-*.so*"
|
||||
"$pangoft2_libdir" "libpangoft2-*.so*"
|
||||
)
|
||||
LIBRARIES=()
|
||||
for (( i=0; i<${#FIND_ARRAY[@]}; i+=2 )); do
|
||||
directory=${FIND_ARRAY[i]}
|
||||
library=${FIND_ARRAY[i+1]}
|
||||
while IFS= read -r -d '' file; do
|
||||
LIBRARIES+=( "--library=$file" )
|
||||
done < <(find "$directory" \( -type l -o -type f \) -name "$library" -print0)
|
||||
done
|
||||
|
||||
env LINUXDEPLOY_PLUGIN_MODE=1 "$LINUXDEPLOY" --appdir="$APPDIR" "${LIBRARIES[@]}"
|
||||
|
||||
# Create symbolic links as a workaround
|
||||
# Details: https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/issues/24#issuecomment-1030026529
|
||||
echo "Manually setting rpath for GTK modules"
|
||||
PATCH_ARRAY=(
|
||||
"$gtk3_immodulesdir"
|
||||
"$gtk3_printbackendsdir"
|
||||
"$gdk_pixbuf_moduledir"
|
||||
)
|
||||
for directory in "${PATCH_ARRAY[@]}"; do
|
||||
while IFS= read -r -d '' file; do
|
||||
ln $verbose -s "${file/\/usr\/lib\//}" "$APPDIR/usr/lib"
|
||||
done < <(find "$directory" -name '*.so' -print0)
|
||||
done
|
||||
|
||||
# set write permission on lib64 again to make it deletable.
|
||||
chmod +w "$APPDIR"/usr/lib64 || true
|
||||
|
||||
# We have to copy the files first to not get permission errors when we assign gio_extras_dir
|
||||
find /usr/lib* -name libgiognutls.so -exec mkdir -p "$APPDIR"/"$(dirname '{}')" \; -exec cp --parents '{}' "$APPDIR/" \; || true
|
||||
# related files that we seemingly don't need:
|
||||
# libgiolibproxy.so - libgiognomeproxy.so - glib-pacrunner
|
||||
|
||||
gio_extras_dir=$(find "$APPDIR"/usr/lib* -name libgiognutls.so -exec dirname '{}' \; 2>/dev/null)
|
||||
cat >> "$HOOKFILE" <<EOF
|
||||
export GIO_EXTRA_MODULES="\$APPDIR/${gio_extras_dir#"$APPDIR"/}"
|
||||
EOF
|
||||
|
||||
#binary patch absolute paths in libwebkit files
|
||||
find "$APPDIR"/usr/lib* -name 'libwebkit*' -exec sed -i -e "s|/usr|././|g" '{}' \;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
use super::debian;
|
||||
use crate::{
|
||||
bundle::settings::Arch,
|
||||
error::{Context, ErrorExt},
|
||||
utils::{fs_utils, http_utils::download, CommandExt},
|
||||
Settings,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
@@ -52,7 +52,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
fs::create_dir_all(&tools_path)?;
|
||||
|
||||
let linuxdeploy_path = prepare_tools(&tools_path, tools_arch)?;
|
||||
let linuxdeploy_path = prepare_tools(
|
||||
&tools_path,
|
||||
tools_arch,
|
||||
settings.log_level() != log::Level::Error,
|
||||
)?;
|
||||
|
||||
let package_dir = settings.project_out_directory().join("bundle/appimage_deb");
|
||||
|
||||
@@ -120,13 +124,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// xdg-open will be handled by the `files` config instead
|
||||
if settings.deep_link_protocols().is_some() && !app_dir_usr_bin.join("xdg-open").exists() {
|
||||
fs::copy("/usr/bin/xdg-mime", app_dir_usr_bin.join("xdg-mime"))
|
||||
.context("xdg-mime binary not found")?;
|
||||
.fs_context("xdg-mime binary not found", "/usr/bin/xdg-mime".to_string())?;
|
||||
}
|
||||
|
||||
// we also check if the user may have provided their own copy already
|
||||
if settings.appimage().bundle_xdg_open && !app_dir_usr_bin.join("xdg-open").exists() {
|
||||
fs::copy("/usr/bin/xdg-open", app_dir_usr_bin.join("xdg-open"))
|
||||
.context("xdg-open binary not found")?;
|
||||
.fs_context("xdg-open binary not found", "/usr/bin/xdg-open".to_string())?;
|
||||
}
|
||||
|
||||
let search_dirs = [
|
||||
@@ -186,6 +190,8 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut cmd = Command::new(linuxdeploy_path);
|
||||
cmd.env("OUTPUT", &appimage_path);
|
||||
cmd.env("ARCH", tools_arch);
|
||||
// Looks like the cli arg isn't enough for the updated AppImage output-plugin.
|
||||
cmd.env("APPIMAGE_EXTRACT_AND_RUN", "1");
|
||||
cmd.args([
|
||||
"--appimage-extract-and-run",
|
||||
"--verbosity",
|
||||
@@ -217,34 +223,49 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
}
|
||||
|
||||
// returns the linuxdeploy path to keep linuxdeploy_arch contained
|
||||
fn prepare_tools(tools_path: &Path, arch: &str) -> crate::Result<PathBuf> {
|
||||
fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result<PathBuf> {
|
||||
let apprun = tools_path.join(format!("AppRun-{arch}"));
|
||||
if !apprun.exists() {
|
||||
let data = download(&format!(
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}"
|
||||
))?;
|
||||
write_and_make_executable(&apprun, data)?;
|
||||
write_and_make_executable(&apprun, &data)?;
|
||||
}
|
||||
|
||||
let linuxdeploy_arch = if arch == "i686" { "i383" } else { arch };
|
||||
let linuxdeploy = tools_path.join(format!("linuxdeploy-{linuxdeploy_arch}.AppImage"));
|
||||
if !linuxdeploy.exists() {
|
||||
let data = download(&format!("https://github.com/tauri-apps/binary-releases/releases/download/linuxdeploy/linuxdeploy-{linuxdeploy_arch}.AppImage"))?;
|
||||
write_and_make_executable(&linuxdeploy, data)?;
|
||||
write_and_make_executable(&linuxdeploy, &data)?;
|
||||
}
|
||||
|
||||
let gtk = tools_path.join("linuxdeploy-plugin-gtk.sh");
|
||||
if !gtk.exists() {
|
||||
let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh")?;
|
||||
let data = include_bytes!("./linuxdeploy-plugin-gtk.sh");
|
||||
write_and_make_executable(>k, data)?;
|
||||
}
|
||||
|
||||
let gstreamer = tools_path.join("linuxdeploy-plugin-gstreamer.sh");
|
||||
if !gstreamer.exists() {
|
||||
let data = download("https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh")?;
|
||||
let data = include_bytes!("./linuxdeploy-plugin-gstreamer.sh");
|
||||
write_and_make_executable(&gstreamer, data)?;
|
||||
}
|
||||
|
||||
let appimage = tools_path.join("linuxdeploy-plugin-appimage.AppImage");
|
||||
if !appimage.exists() {
|
||||
// This is optional, linuxdeploy will fall back to its built-in version if the download failed.
|
||||
let data = download(&format!("https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-{arch}.AppImage"));
|
||||
match data {
|
||||
Ok(data) => write_and_make_executable(&appimage, &data)?,
|
||||
Err(err) => {
|
||||
log::error!("Download of AppImage plugin failed. Using older built-in version instead.");
|
||||
if verbose {
|
||||
log::debug!("{err:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should prevent linuxdeploy to be detected by appimage integration tools
|
||||
let _ = Command::new("dd")
|
||||
.args([
|
||||
@@ -260,7 +281,7 @@ fn prepare_tools(tools_path: &Path, arch: &str) -> crate::Result<PathBuf> {
|
||||
Ok(linuxdeploy)
|
||||
}
|
||||
|
||||
fn write_and_make_executable(path: &Path, data: Vec<u8>) -> std::io::Result<()> {
|
||||
fn write_and_make_executable(path: &Path, data: &[u8]) -> std::io::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
fs::write(path, data)?;
|
||||
@@ -24,8 +24,12 @@
|
||||
// generate postinst or prerm files.
|
||||
|
||||
use super::freedesktop;
|
||||
use crate::{bundle::settings::Arch, utils::fs_utils, Settings};
|
||||
use anyhow::Context;
|
||||
use crate::{
|
||||
bundle::settings::Arch,
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
Settings,
|
||||
};
|
||||
use flate2::{write::GzEncoder, Compression};
|
||||
use tar::HeaderMode;
|
||||
use walkdir::WalkDir;
|
||||
@@ -64,30 +68,32 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let base_dir = settings.project_out_directory().join("bundle/deb");
|
||||
let package_dir = base_dir.join(&package_base_name);
|
||||
if package_dir.exists() {
|
||||
fs::remove_dir_all(&package_dir)
|
||||
.with_context(|| format!("Failed to remove old {package_base_name}"))?;
|
||||
fs::remove_dir_all(&package_dir).fs_context(
|
||||
"Failed to Remove old package directory",
|
||||
package_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
let package_path = base_dir.join(&package_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
|
||||
|
||||
let (data_dir, _) = generate_data(settings, &package_dir)
|
||||
.with_context(|| "Failed to build data folders and files")?;
|
||||
let (data_dir, _) =
|
||||
generate_data(settings, &package_dir).context("Failed to build data folders and files")?;
|
||||
fs_utils::copy_custom_files(&settings.deb().files, &data_dir)
|
||||
.with_context(|| "Failed to copy custom files")?;
|
||||
.context("Failed to copy custom files")?;
|
||||
|
||||
// Generate control files.
|
||||
let control_dir = package_dir.join("control");
|
||||
generate_control_file(settings, arch, &control_dir, &data_dir)
|
||||
.with_context(|| "Failed to create control file")?;
|
||||
generate_scripts(settings, &control_dir).with_context(|| "Failed to create control scripts")?;
|
||||
generate_md5sums(&control_dir, &data_dir).with_context(|| "Failed to create md5sums file")?;
|
||||
.context("Failed to create control file")?;
|
||||
generate_scripts(settings, &control_dir).context("Failed to create control scripts")?;
|
||||
generate_md5sums(&control_dir, &data_dir).context("Failed to create md5sums file")?;
|
||||
|
||||
// Generate `debian-binary` file; see
|
||||
// http://www.tldp.org/HOWTO/Debian-Binary-Package-Building-HOWTO/x60.html#AEN66
|
||||
let debian_binary_path = package_dir.join("debian-binary");
|
||||
create_file_with_data(&debian_binary_path, "2.0\n")
|
||||
.with_context(|| "Failed to create debian-binary file")?;
|
||||
.context("Failed to create debian-binary file")?;
|
||||
|
||||
// Apply tar/gzip/ar to create the final package file.
|
||||
let control_tar_gz_path =
|
||||
|
||||
@@ -21,12 +21,12 @@ use std::fs::{read_to_string, File};
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use handlebars::Handlebars;
|
||||
use image::{self, codecs::png::PngDecoder, ImageDecoder};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
error::Context,
|
||||
utils::{self, fs_utils},
|
||||
Settings,
|
||||
};
|
||||
@@ -114,11 +114,13 @@ pub fn generate_desktop_file(
|
||||
if let Some(template) = custom_template_path {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", read_to_string(template)?)
|
||||
.with_context(|| "Failed to setup custom handlebar template")?;
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup custom handlebar template")?;
|
||||
} else {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", include_str!("./main.desktop"))
|
||||
.with_context(|| "Failed to setup default handlebar template")?;
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup default handlebar template")?;
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{bundle::settings::Arch, Settings};
|
||||
use crate::{bundle::settings::Arch, error::ErrorExt, Settings};
|
||||
|
||||
use anyhow::Context;
|
||||
use rpm::{self, signature::pgp, Dependency, FileMode, FileOptions};
|
||||
use std::{
|
||||
env,
|
||||
@@ -21,7 +20,10 @@ use super::freedesktop;
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let product_name = settings.product_name();
|
||||
let version = settings.version_string();
|
||||
let release = settings.rpm().release.as_str();
|
||||
let release = match settings.rpm().release.as_str() {
|
||||
"" => "1", // Considered the default. If left empty, you get file with "-.".
|
||||
v => v,
|
||||
};
|
||||
let epoch = settings.rpm().epoch;
|
||||
let arch = match settings.binary_arch() {
|
||||
Arch::X86_64 => "x86_64",
|
||||
@@ -45,10 +47,13 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let base_dir = settings.project_out_directory().join("bundle/rpm");
|
||||
let package_dir = base_dir.join(&package_base_name);
|
||||
if package_dir.exists() {
|
||||
fs::remove_dir_all(&package_dir)
|
||||
.with_context(|| format!("Failed to remove old {package_base_name}"))?;
|
||||
fs::remove_dir_all(&package_dir).fs_context(
|
||||
"Failed to remove old package directory",
|
||||
package_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
fs::create_dir_all(&package_dir)?;
|
||||
fs::create_dir_all(&package_dir)
|
||||
.fs_context("Failed to create package directory", package_dir.clone())?;
|
||||
let package_path = base_dir.join(&package_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", package_name, package_path.display());
|
||||
@@ -234,6 +239,5 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
let mut f = fs::File::create(&package_path)?;
|
||||
pkg.write(&mut f)?;
|
||||
|
||||
Ok(vec![package_path])
|
||||
}
|
||||
|
||||
@@ -24,15 +24,16 @@
|
||||
|
||||
use super::{
|
||||
icon::create_icns_file,
|
||||
sign::{notarize, notarize_auth, sign, NotarizeAuthError, SignTarget},
|
||||
sign::{notarize, notarize_auth, notarize_without_stapling, sign, SignTarget},
|
||||
};
|
||||
use crate::{
|
||||
bundle::settings::PlistKind,
|
||||
error::{Context, ErrorExt, NotarizeAuthError},
|
||||
utils::{fs_utils, CommandExt},
|
||||
Error::GenericError,
|
||||
Settings,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
@@ -65,11 +66,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to remove old {app_product_name}"))?;
|
||||
.fs_context("failed to remove old app bundle", &app_bundle_path)?;
|
||||
}
|
||||
let bundle_directory = app_bundle_path.join("Contents");
|
||||
fs::create_dir_all(&bundle_directory)
|
||||
.with_context(|| format!("Failed to create bundle directory at {bundle_directory:?}"))?;
|
||||
.fs_context("failed to create bundle directory", &bundle_directory)?;
|
||||
|
||||
let resources_dir = bundle_directory.join("Resources");
|
||||
let bin_dir = bundle_directory.join("MacOS");
|
||||
@@ -103,7 +104,11 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_directory, settings)?;
|
||||
|
||||
if let Some(keychain) = super::sign::keychain(settings.macos().signing_identity.as_deref())? {
|
||||
if settings.no_sign() {
|
||||
log::warn!("Skipping signing due to --no-sign flag.",);
|
||||
} else if let Some(keychain) =
|
||||
super::sign::keychain(settings.macos().signing_identity.as_deref())?
|
||||
{
|
||||
// Sign frameworks and sidecar binaries first, per apple, signing must be done inside out
|
||||
// https://developer.apple.com/forums/thread/701514
|
||||
sign_paths.push(SignTarget {
|
||||
@@ -121,11 +126,15 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// notarization is required for distribution
|
||||
match notarize_auth() {
|
||||
Ok(auth) => {
|
||||
notarize(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
if settings.macos().skip_stapling {
|
||||
notarize_without_stapling(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
} else {
|
||||
notarize(&keychain, app_bundle_path.clone(), &auth)?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if matches!(e, NotarizeAuthError::MissingTeamId) {
|
||||
return Err(anyhow::anyhow!("{e}").into());
|
||||
return Err(e.into());
|
||||
} else {
|
||||
log::warn!("skipping app notarization, {}", e.to_string());
|
||||
}
|
||||
@@ -165,6 +174,12 @@ fn copy_binaries_to_bundle(
|
||||
/// Copies user-defined files to the app under Contents.
|
||||
fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) -> crate::Result<()> {
|
||||
for (contents_path, path) in settings.macos().files.iter() {
|
||||
if !path.try_exists()? {
|
||||
return Err(GenericError(format!(
|
||||
"Failed to copy {path:?} to {contents_path:?}. {path:?} does not exist."
|
||||
)));
|
||||
}
|
||||
|
||||
let contents_path = if contents_path.is_absolute() {
|
||||
contents_path.strip_prefix("/").unwrap()
|
||||
} else {
|
||||
@@ -173,9 +188,13 @@ fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) ->
|
||||
if path.is_file() {
|
||||
fs_utils::copy_file(path, &bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy file {path:?} to {contents_path:?}"))?;
|
||||
} else {
|
||||
} else if path.is_dir() {
|
||||
fs_utils::copy_dir(path, &bundle_directory.join(contents_path))
|
||||
.with_context(|| format!("Failed to copy directory {path:?} to {contents_path:?}"))?;
|
||||
} else {
|
||||
return Err(GenericError(format!(
|
||||
"{path:?} is not a file or directory."
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -245,6 +264,55 @@ fn create_info_plist(
|
||||
}
|
||||
|
||||
if let Some(associations) = settings.file_associations() {
|
||||
let exported_associations = associations
|
||||
.iter()
|
||||
.filter_map(|association| {
|
||||
association.exported_type.as_ref().map(|exported_type| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
|
||||
dict.insert(
|
||||
"UTTypeIdentifier".into(),
|
||||
exported_type.identifier.clone().into(),
|
||||
);
|
||||
if let Some(description) = &association.description {
|
||||
dict.insert("UTTypeDescription".into(), description.clone().into());
|
||||
}
|
||||
if let Some(conforms_to) = &exported_type.conforms_to {
|
||||
dict.insert(
|
||||
"UTTypeConformsTo".into(),
|
||||
plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
|
||||
);
|
||||
}
|
||||
|
||||
let mut specification = plist::Dictionary::new();
|
||||
specification.insert(
|
||||
"public.filename-extension".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|s| s.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
if let Some(mime_type) = &association.mime_type {
|
||||
specification.insert("public.mime-type".into(), mime_type.clone().into());
|
||||
}
|
||||
|
||||
dict.insert("UTTypeTagSpecification".into(), specification.into());
|
||||
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !exported_associations.is_empty() {
|
||||
plist.insert(
|
||||
"UTExportedTypeDeclarations".into(),
|
||||
plist::Value::Array(exported_associations),
|
||||
);
|
||||
}
|
||||
|
||||
plist.insert(
|
||||
"CFBundleDocumentTypes".into(),
|
||||
plist::Value::Array(
|
||||
@@ -252,16 +320,27 @@ fn create_info_plist(
|
||||
.iter()
|
||||
.map(|association| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
dict.insert(
|
||||
"CFBundleTypeExtensions".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|ext| ext.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
|
||||
if !association.ext.is_empty() {
|
||||
dict.insert(
|
||||
"CFBundleTypeExtensions".into(),
|
||||
plist::Value::Array(
|
||||
association
|
||||
.ext
|
||||
.iter()
|
||||
.map(|ext| ext.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(content_types) = &association.content_types {
|
||||
dict.insert(
|
||||
"LSItemContentTypes".into(),
|
||||
plist::Value::Array(content_types.iter().map(|s| s.to_string().into()).collect()),
|
||||
);
|
||||
}
|
||||
|
||||
dict.insert(
|
||||
"CFBundleTypeName".into(),
|
||||
association
|
||||
@@ -289,6 +368,7 @@ fn create_info_plist(
|
||||
plist::Value::Array(
|
||||
protocols
|
||||
.iter()
|
||||
.filter(|p| !p.schemes.is_empty())
|
||||
.map(|protocol| {
|
||||
let mut dict = plist::Dictionary::new();
|
||||
dict.insert(
|
||||
@@ -339,8 +419,11 @@ fn create_info_plist(
|
||||
plist.insert("NSAppTransportSecurity".into(), security.into());
|
||||
}
|
||||
|
||||
if let Some(user_plist_path) = &settings.macos().info_plist_path {
|
||||
let user_plist = plist::Value::from_file(user_plist_path)?;
|
||||
if let Some(user_plist) = &settings.macos().info_plist {
|
||||
let user_plist = match user_plist {
|
||||
PlistKind::Path(path) => plist::Value::from_file(path)?,
|
||||
PlistKind::Plist(value) => value.clone(),
|
||||
};
|
||||
if let Some(dict) = user_plist.into_dictionary() {
|
||||
for (key, value) in dict {
|
||||
plist.insert(key, value);
|
||||
@@ -372,18 +455,12 @@ fn copy_frameworks_to_bundle(
|
||||
) -> crate::Result<Vec<SignTarget>> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
let frameworks = settings
|
||||
.macos()
|
||||
.frameworks
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let frameworks = settings.macos().frameworks.clone().unwrap_or_default();
|
||||
if frameworks.is_empty() {
|
||||
return Ok(paths);
|
||||
}
|
||||
let dest_dir = bundle_directory.join("Frameworks");
|
||||
fs::create_dir_all(bundle_directory)
|
||||
.with_context(|| format!("Failed to create Frameworks directory at {dest_dir:?}"))?;
|
||||
fs::create_dir_all(&dest_dir).fs_context("failed to create Frameworks directory", &dest_dir)?;
|
||||
for framework in frameworks.iter() {
|
||||
if framework.ends_with(".framework") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
@@ -397,9 +474,7 @@ fn copy_frameworks_to_bundle(
|
||||
} else if framework.ends_with(".dylib") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
if !src_path.exists() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"Library not found: {framework}"
|
||||
)));
|
||||
return Err(GenericError(format!("Library not found: {framework}")));
|
||||
}
|
||||
let src_name = src_path.file_name().expect("Couldn't get library filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
@@ -410,7 +485,7 @@ fn copy_frameworks_to_bundle(
|
||||
});
|
||||
continue;
|
||||
} else if framework.contains('/') {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
return Err(GenericError(format!(
|
||||
"Framework path should have .framework extension: {framework}"
|
||||
)));
|
||||
}
|
||||
@@ -428,7 +503,7 @@ fn copy_frameworks_to_bundle(
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
return Err(GenericError(format!(
|
||||
"Could not locate framework: {framework}"
|
||||
)));
|
||||
}
|
||||
@@ -521,3 +596,153 @@ fn add_nested_code_sign_path(src_path: &Path, dest_path: &Path, sign_paths: &mut
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::bundle::{BundleSettings, MacOsSettings, PackageSettings, SettingsBuilder};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Helper that builds a `Settings` instance and bundle directory for tests.
|
||||
/// It receives a mapping of bundle-relative paths to source paths and
|
||||
/// returns the generated bundle directory and settings.
|
||||
fn create_test_bundle(
|
||||
project_dir: &Path,
|
||||
files: HashMap<PathBuf, PathBuf>,
|
||||
) -> (PathBuf, crate::bundle::Settings) {
|
||||
let macos_settings = MacOsSettings {
|
||||
files,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = SettingsBuilder::new()
|
||||
.project_out_directory(project_dir)
|
||||
.package_settings(PackageSettings {
|
||||
product_name: "TestApp".into(),
|
||||
version: "0.1.0".into(),
|
||||
description: "test".into(),
|
||||
homepage: None,
|
||||
authors: None,
|
||||
default_run: None,
|
||||
})
|
||||
.bundle_settings(BundleSettings {
|
||||
macos: macos_settings,
|
||||
..Default::default()
|
||||
})
|
||||
.target("x86_64-apple-darwin".into())
|
||||
.build()
|
||||
.expect("failed to build settings");
|
||||
|
||||
let bundle_dir = project_dir.join("TestApp.app/Contents");
|
||||
fs::create_dir_all(&bundle_dir).expect("failed to create bundle dir");
|
||||
|
||||
(bundle_dir, settings)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_file_to_bundle_file() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Prepare a single file to copy.
|
||||
let src_file = tmp_dir.path().join("sample.txt");
|
||||
fs::write(&src_file, b"hello tauri").expect("failed to write sample file");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Resources/sample.txt"), src_file.clone())]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_dir, &settings)
|
||||
.expect("copy_custom_files_to_bundle failed");
|
||||
|
||||
let dest_file = bundle_dir.join("Resources/sample.txt");
|
||||
assert!(dest_file.exists() && dest_file.is_file());
|
||||
assert_eq!(fs::read_to_string(dest_file).unwrap(), "hello tauri");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_file_to_bundle_dir() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Create a source directory with a nested file.
|
||||
let src_dir = tmp_dir.path().join("assets");
|
||||
fs::create_dir_all(&src_dir).expect("failed to create assets directory");
|
||||
let nested_file = src_dir.join("nested.txt");
|
||||
fs::write(&nested_file, b"nested").expect("failed to write nested file");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("MyAssets"), src_dir.clone())]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_dir, &settings)
|
||||
.expect("copy_custom_files_to_bundle failed");
|
||||
|
||||
let dest_nested_file = bundle_dir.join("MyAssets/nested.txt");
|
||||
assert!(
|
||||
dest_nested_file.exists(),
|
||||
"{dest_nested_file:?} does not exist"
|
||||
);
|
||||
assert!(
|
||||
dest_nested_file.is_file(),
|
||||
"{dest_nested_file:?} is not a file"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read_to_string(dest_nested_file).unwrap().trim(),
|
||||
"nested"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_missing_source() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
// Intentionally reference a non-existent path.
|
||||
let missing_path = tmp_dir.path().join("does_not_exist.txt");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Missing.txt"), missing_path)]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("does not exist"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_invalid_source() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Invalid.txt"), PathBuf::from("///"))]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("Failed to copy directory"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_copy_custom_files_to_bundle_dev_null() {
|
||||
let tmp_dir = tempfile::tempdir().expect("failed to create temp dir");
|
||||
|
||||
let files_map = HashMap::from([(PathBuf::from("Invalid.txt"), PathBuf::from("/dev/null"))]);
|
||||
|
||||
let (bundle_dir, settings) = create_test_bundle(tmp_dir.path(), files_map);
|
||||
|
||||
let result = copy_custom_files_to_bundle(&bundle_dir, &settings);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("is not a file or directory."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
use super::{app, icon::create_icns_file};
|
||||
use crate::{
|
||||
bundle::{settings::Arch, Bundle},
|
||||
error::{Context, ErrorExt},
|
||||
utils::CommandExt,
|
||||
PackageType, Settings,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, write},
|
||||
@@ -68,10 +67,9 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
|
||||
for path in &[&support_directory_path, &output_path] {
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path).with_context(|| format!("Failed to remove old {dmg_name}"))?;
|
||||
fs::remove_dir_all(path).fs_context("failed to remove old dmg", path.to_path_buf())?;
|
||||
}
|
||||
fs::create_dir_all(path)
|
||||
.with_context(|| format!("Failed to create output directory at {path:?}"))?;
|
||||
fs::create_dir_all(path).fs_context("failed to create output directory", path.to_path_buf())?;
|
||||
}
|
||||
|
||||
// create paths for script
|
||||
@@ -195,7 +193,7 @@ pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<
|
||||
// Sign DMG if needed
|
||||
// skipping self-signing DMGs https://github.com/tauri-apps/tauri/issues/12288
|
||||
let identity = settings.macos().signing_identity.as_deref();
|
||||
if identity != Some("-") {
|
||||
if !settings.no_sign() && identity != Some("-") {
|
||||
if let Some(keychain) = super::sign::keychain(identity)? {
|
||||
super::sign::sign(
|
||||
&keychain,
|
||||
|
||||
@@ -14,11 +14,11 @@
|
||||
// explanation.
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
utils::{self, fs_utils},
|
||||
Settings,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use image::{codecs::png::PngDecoder, GenericImageView, ImageDecoder};
|
||||
|
||||
use std::{
|
||||
@@ -45,10 +45,10 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to remove old {app_product_name}"))?;
|
||||
.fs_context("failed to remove old app bundle", &app_bundle_path)?;
|
||||
}
|
||||
fs::create_dir_all(&app_bundle_path)
|
||||
.with_context(|| format!("Failed to create bundle directory at {app_bundle_path:?}"))?;
|
||||
.fs_context("failed to create bundle directory", &app_bundle_path)?;
|
||||
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
use std::{
|
||||
env::{var, var_os},
|
||||
ffi::OsString,
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::Settings;
|
||||
use crate::{error::NotarizeAuthError, Entitlements, Settings};
|
||||
|
||||
pub struct SignTarget {
|
||||
pub path: PathBuf,
|
||||
@@ -23,11 +23,14 @@ pub fn keychain(identity: Option<&str>) -> crate::Result<Option<tauri_macos_sign
|
||||
) {
|
||||
// import user certificate - useful for for CI build
|
||||
let keychain =
|
||||
tauri_macos_sign::Keychain::with_certificate(&certificate_encoded, &certificate_password)?;
|
||||
tauri_macos_sign::Keychain::with_certificate(&certificate_encoded, &certificate_password)
|
||||
.map_err(Box::new)?;
|
||||
if let Some(identity) = identity {
|
||||
let certificate_identity = keychain.signing_identity();
|
||||
if !certificate_identity.contains(identity) {
|
||||
return Err(anyhow::anyhow!("certificate from APPLE_CERTIFICATE \"{certificate_identity}\" environment variable does not match provided identity \"{identity}\"").into());
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"certificate from APPLE_CERTIFICATE \"{certificate_identity}\" environment variable does not match provided identity \"{identity}\""
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Some(keychain))
|
||||
@@ -48,16 +51,23 @@ pub fn sign(
|
||||
log::info!(action = "Signing"; "with identity \"{}\"", keychain.signing_identity());
|
||||
|
||||
for target in targets {
|
||||
let entitlements_path = if target.is_an_executable {
|
||||
settings.macos().entitlements.as_ref().map(Path::new)
|
||||
} else {
|
||||
None
|
||||
let (entitlements_path, _temp_file) = match settings.macos().entitlements.as_ref() {
|
||||
Some(Entitlements::Path(path)) => (Some(path.to_owned()), None),
|
||||
Some(Entitlements::Plist(plist)) => {
|
||||
let mut temp_file = tempfile::NamedTempFile::new()?;
|
||||
plist::to_writer_xml(temp_file.as_file_mut(), &plist)?;
|
||||
(Some(temp_file.path().to_path_buf()), Some(temp_file))
|
||||
}
|
||||
None => (None, None),
|
||||
};
|
||||
keychain.sign(
|
||||
&target.path,
|
||||
entitlements_path,
|
||||
target.is_an_executable && settings.macos().hardened_runtime,
|
||||
)?;
|
||||
|
||||
keychain
|
||||
.sign(
|
||||
&target.path,
|
||||
entitlements_path.as_deref(),
|
||||
target.is_an_executable && settings.macos().hardened_runtime,
|
||||
)
|
||||
.map_err(Box::new)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -68,17 +78,19 @@ pub fn notarize(
|
||||
app_bundle_path: PathBuf,
|
||||
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
|
||||
) -> crate::Result<()> {
|
||||
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials).map_err(Into::into)
|
||||
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials)
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotarizeAuthError {
|
||||
#[error(
|
||||
"The team ID is now required for notarization with app-specific password as authentication. Please set the `APPLE_TEAM_ID` environment variable. You can find the team ID in https://developer.apple.com/account#MembershipDetailsCard."
|
||||
)]
|
||||
MissingTeamId,
|
||||
#[error(transparent)]
|
||||
Anyhow(#[from] anyhow::Error),
|
||||
pub fn notarize_without_stapling(
|
||||
keychain: &tauri_macos_sign::Keychain,
|
||||
app_bundle_path: PathBuf,
|
||||
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
|
||||
) -> crate::Result<()> {
|
||||
tauri_macos_sign::notarize_without_stapling(keychain, &app_bundle_path, credentials)
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials, NotarizeAuthError>
|
||||
@@ -97,10 +109,18 @@ pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials,
|
||||
}
|
||||
(Some(_apple_id), Some(_password), None) => Err(NotarizeAuthError::MissingTeamId),
|
||||
_ => {
|
||||
match (var_os("APPLE_API_KEY"), var_os("APPLE_API_ISSUER"), var("APPLE_API_KEY_PATH")) {
|
||||
match (
|
||||
var_os("APPLE_API_KEY"),
|
||||
var_os("APPLE_API_ISSUER"),
|
||||
var("APPLE_API_KEY_PATH"),
|
||||
) {
|
||||
(Some(key_id), Some(issuer), Ok(key_path)) => {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path( key_path.into()), issuer })
|
||||
},
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey {
|
||||
key_id,
|
||||
key: tauri_macos_sign::ApiKey::Path(key_path.into()),
|
||||
issuer,
|
||||
})
|
||||
}
|
||||
(Some(key_id), Some(issuer), Err(_)) => {
|
||||
let mut api_key_file_name = OsString::from("AuthKey_");
|
||||
api_key_file_name.push(&key_id);
|
||||
@@ -122,12 +142,18 @@ pub fn notarize_auth() -> Result<tauri_macos_sign::AppleNotarizationCredentials,
|
||||
}
|
||||
|
||||
if let Some(key_path) = key_path {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey { key_id, key: tauri_macos_sign::ApiKey::Path(key_path), issuer })
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey {
|
||||
key_id,
|
||||
key: tauri_macos_sign::ApiKey::Path(key_path),
|
||||
issuer,
|
||||
})
|
||||
} else {
|
||||
Err(anyhow::anyhow!("could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {api_key_file_name:?} file").into())
|
||||
Err(NotarizeAuthError::MissingApiKey {
|
||||
file_name: api_key_file_name.to_string_lossy().into_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("no APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found").into())
|
||||
_ => Err(NotarizeAuthError::MissingCredentials),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::category::AppCategory;
|
||||
use crate::{bundle::platform::target_triple, utils::fs_utils};
|
||||
use anyhow::Context;
|
||||
use crate::{bundle::platform::target_triple, error::Context, utils::fs_utils};
|
||||
pub use tauri_utils::config::WebviewInstallMode;
|
||||
use tauri_utils::{
|
||||
config::{
|
||||
@@ -346,16 +345,43 @@ pub struct MacOsSettings {
|
||||
pub exception_domain: Option<String>,
|
||||
/// Code signing identity.
|
||||
pub signing_identity: Option<String>,
|
||||
/// Whether to wait for notarization to finish and `staple` the ticket onto the app.
|
||||
///
|
||||
/// Gatekeeper will look for stapled tickets to tell whether your app was notarized without
|
||||
/// reaching out to Apple's servers which is helpful in offline environments.
|
||||
///
|
||||
/// Enabling this option will also result in `tauri build` not waiting for notarization to finish
|
||||
/// which is helpful for the very first time your app is notarized as this can take multiple hours.
|
||||
/// On subsequent runs, it's recommended to disable this setting again.
|
||||
pub skip_stapling: bool,
|
||||
/// Preserve the hardened runtime version flag, see <https://developer.apple.com/documentation/security/hardened_runtime>
|
||||
///
|
||||
/// Settings this to `false` is useful when using an ad-hoc signature, making it less strict.
|
||||
pub hardened_runtime: bool,
|
||||
/// Provider short name for notarization.
|
||||
pub provider_short_name: Option<String>,
|
||||
/// Path or contents of the entitlements.plist file.
|
||||
pub entitlements: Option<Entitlements>,
|
||||
/// Path to the Info.plist file or raw plist value to merge with the bundle Info.plist.
|
||||
pub info_plist: Option<PlistKind>,
|
||||
}
|
||||
|
||||
/// Entitlements for macOS code signing.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Entitlements {
|
||||
/// Path to the entitlements.plist file.
|
||||
pub entitlements: Option<String>,
|
||||
/// Path to the Info.plist file for the bundle.
|
||||
pub info_plist_path: Option<PathBuf>,
|
||||
Path(PathBuf),
|
||||
/// Raw plist::Value.
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
/// Plist format.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PlistKind {
|
||||
/// Path to a .plist file.
|
||||
Path(PathBuf),
|
||||
/// Raw plist value.
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
/// Configuration for a target language for the WiX build.
|
||||
@@ -563,6 +589,12 @@ pub struct WindowsSettings {
|
||||
pub sign_command: Option<CustomSignCommandSettings>,
|
||||
}
|
||||
|
||||
impl WindowsSettings {
|
||||
pub(crate) fn can_sign(&self) -> bool {
|
||||
self.sign_command.is_some() || self.certificate_thumbprint.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
mod _default {
|
||||
use super::*;
|
||||
@@ -769,6 +801,8 @@ pub struct Settings {
|
||||
target_platform: TargetPlatform,
|
||||
/// The target triple.
|
||||
target: String,
|
||||
/// Whether to disable code signing during the bundling process.
|
||||
no_sign: bool,
|
||||
}
|
||||
|
||||
/// A builder for [`Settings`].
|
||||
@@ -782,6 +816,7 @@ pub struct SettingsBuilder {
|
||||
binaries: Vec<BundleBinary>,
|
||||
target: Option<String>,
|
||||
local_tools_directory: Option<PathBuf>,
|
||||
no_sign: bool,
|
||||
}
|
||||
|
||||
impl SettingsBuilder {
|
||||
@@ -851,6 +886,13 @@ impl SettingsBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether to skip code signing.
|
||||
#[must_use]
|
||||
pub fn no_sign(mut self, no_sign: bool) -> Self {
|
||||
self.no_sign = no_sign;
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a Settings from the CLI args.
|
||||
///
|
||||
/// Package settings will be read from Cargo.toml.
|
||||
@@ -885,6 +927,7 @@ impl SettingsBuilder {
|
||||
},
|
||||
target_platform,
|
||||
target,
|
||||
no_sign: self.no_sign,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -943,7 +986,6 @@ impl Settings {
|
||||
.iter()
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the file name of the binary being bundled.
|
||||
@@ -953,7 +995,6 @@ impl Settings {
|
||||
.iter_mut()
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the file name of the binary being bundled.
|
||||
@@ -964,7 +1005,6 @@ impl Settings {
|
||||
.find(|bin| bin.main)
|
||||
.context("failed to find main binary, make sure you have a `package > default-run` in the Cargo.toml file")
|
||||
.map(|b| b.name())
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the path to the specified binary.
|
||||
@@ -1233,4 +1273,14 @@ impl Settings {
|
||||
pub fn updater(&self) -> Option<&UpdaterSettings> {
|
||||
self.bundle_settings.updater.as_ref()
|
||||
}
|
||||
|
||||
/// Whether to skip signing.
|
||||
pub fn no_sign(&self) -> bool {
|
||||
self.no_sign
|
||||
}
|
||||
|
||||
/// Set whether to skip signing.
|
||||
pub fn set_no_sign(&mut self, no_sign: bool) {
|
||||
self.no_sign = no_sign;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{
|
||||
},
|
||||
Bundle,
|
||||
},
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
Settings,
|
||||
};
|
||||
@@ -22,7 +23,6 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
// Build update
|
||||
@@ -216,7 +216,9 @@ pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
|
||||
.unix_permissions(0o755);
|
||||
|
||||
zip.start_file(file_name.to_string_lossy(), options)?;
|
||||
let mut f = File::open(src_file)?;
|
||||
let mut f =
|
||||
File::open(src_file).fs_context("failed to open updater ZIP file", src_file.to_path_buf())?;
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
f.read_to_end(&mut buffer)?;
|
||||
zip.write_all(&buffer)?;
|
||||
|
||||
@@ -7,20 +7,20 @@ use crate::{
|
||||
bundle::{
|
||||
settings::{Arch, Settings},
|
||||
windows::{
|
||||
sign::try_sign,
|
||||
sign::{should_sign, try_sign},
|
||||
util::{
|
||||
download_webview2_bootstrapper, download_webview2_offline_installer,
|
||||
WIX_OUTPUT_FOLDER_NAME, WIX_UPDATER_OUTPUT_FOLDER_NAME,
|
||||
},
|
||||
},
|
||||
},
|
||||
error::Context,
|
||||
utils::{
|
||||
fs_utils::copy_file,
|
||||
http_utils::{download_and_verify, extract_zip, HashAlgorithm},
|
||||
CommandExt,
|
||||
},
|
||||
};
|
||||
use anyhow::{bail, Context};
|
||||
use handlebars::{html_escape, to_json, Handlebars};
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -279,37 +279,40 @@ fn clear_env_for_wix(cmd: &mut Command) {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_wix_version(version_str: &str) -> anyhow::Result<()> {
|
||||
fn validate_wix_version(version_str: &str) -> crate::Result<()> {
|
||||
let components = version_str
|
||||
.split('.')
|
||||
.flat_map(|c| c.parse::<u64>().ok())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
anyhow::ensure!(
|
||||
components.len() >= 3,
|
||||
"app wix version should be in the format major.minor.patch.build (build is optional)"
|
||||
);
|
||||
if components.len() < 3 {
|
||||
crate::error::bail!(
|
||||
"app wix version should be in the format major.minor.patch.build (build is optional)"
|
||||
);
|
||||
}
|
||||
|
||||
if components[0] > 255 {
|
||||
bail!("app version major number cannot be greater than 255");
|
||||
crate::error::bail!("app version major number cannot be greater than 255");
|
||||
}
|
||||
if components[1] > 255 {
|
||||
bail!("app version minor number cannot be greater than 255");
|
||||
crate::error::bail!("app version minor number cannot be greater than 255");
|
||||
}
|
||||
if components[2] > 65535 {
|
||||
bail!("app version patch number cannot be greater than 65535");
|
||||
crate::error::bail!("app version patch number cannot be greater than 65535");
|
||||
}
|
||||
|
||||
if components.len() == 4 && components[3] > 65535 {
|
||||
bail!("app version build number cannot be greater than 65535");
|
||||
crate::error::bail!("app version build number cannot be greater than 65535");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// WiX requires versions to be numeric only in a `major.minor.patch.build` format
|
||||
fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
let version = semver::Version::parse(version_str).context("invalid app version")?;
|
||||
fn convert_version(version_str: &str) -> crate::Result<String> {
|
||||
let version = semver::Version::parse(version_str)
|
||||
.map_err(Into::into)
|
||||
.context("invalid app version")?;
|
||||
if !version.build.is_empty() {
|
||||
let build = version.build.parse::<u64>();
|
||||
if build.map(|b| b <= 65535).unwrap_or_default() {
|
||||
@@ -318,7 +321,7 @@ fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
version.major, version.minor, version.patch, version.build
|
||||
));
|
||||
} else {
|
||||
bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
crate::error::bail!("optional build metadata in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,7 +333,7 @@ fn convert_version(version_str: &str) -> anyhow::Result<String> {
|
||||
version.major, version.minor, version.patch, version.pre
|
||||
));
|
||||
} else {
|
||||
bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
crate::error::bail!("optional pre-release identifier in app version must be numeric-only and cannot be greater than 65535 for msi target");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,11 +390,7 @@ fn run_candle(
|
||||
cmd.arg(ext);
|
||||
}
|
||||
clear_env_for_wix(&mut cmd);
|
||||
cmd
|
||||
.args(&args)
|
||||
.current_dir(cwd)
|
||||
.output_ok()
|
||||
.context("error running candle.exe")?;
|
||||
cmd.args(&args).current_dir(cwd).output_ok()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -416,11 +415,7 @@ fn run_light(
|
||||
cmd.arg(ext);
|
||||
}
|
||||
clear_env_for_wix(&mut cmd);
|
||||
cmd
|
||||
.args(&args)
|
||||
.current_dir(build_path)
|
||||
.output_ok()
|
||||
.context("error running light.exe")?;
|
||||
cmd.args(&args).current_dir(build_path).output_ok()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -470,10 +465,9 @@ pub fn build_wix_app_installer(
|
||||
fs::create_dir_all(&output_path)?;
|
||||
|
||||
// when we're performing code signing, we'll sign some WiX DLLs, so we make a local copy
|
||||
let wix_toolset_path = if settings.can_sign() {
|
||||
let wix_toolset_path = if settings.windows().can_sign() {
|
||||
let wix_path = output_path.join("wix");
|
||||
crate::utils::fs_utils::copy_dir(wix_toolset_path, &wix_path)
|
||||
.context("failed to copy wix directory")?;
|
||||
crate::utils::fs_utils::copy_dir(wix_toolset_path, &wix_path)?;
|
||||
wix_path
|
||||
} else {
|
||||
wix_toolset_path.to_path_buf()
|
||||
@@ -703,7 +697,9 @@ pub fn build_wix_app_installer(
|
||||
.iter()
|
||||
.flat_map(|p| &p.schemes)
|
||||
.collect::<Vec<_>>();
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
if !schemes.is_empty() {
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(path) = custom_template_path {
|
||||
@@ -771,7 +767,7 @@ pub fn build_wix_app_installer(
|
||||
let mut extensions = Vec::new();
|
||||
for cap in extension_regex.captures_iter(&fragment) {
|
||||
let path = wix_toolset_path.join(format!("Wix{}.dll", &cap[1]));
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&path, settings)?;
|
||||
}
|
||||
extensions.push(path);
|
||||
@@ -785,7 +781,7 @@ pub fn build_wix_app_installer(
|
||||
fragment_extensions.insert(wix_toolset_path.join("WixUtilExtension.dll"));
|
||||
|
||||
// sign default extensions
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
for path in &fragment_extensions {
|
||||
try_sign(path, settings)?;
|
||||
}
|
||||
@@ -879,7 +875,7 @@ pub fn build_wix_app_installer(
|
||||
)?;
|
||||
fs::rename(&msi_output_path, &msi_path)?;
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&msi_path, settings)?;
|
||||
}
|
||||
|
||||
@@ -988,7 +984,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
}
|
||||
added_resources.push(resource_path.clone());
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() && should_sign(&resource_path)? {
|
||||
try_sign(&resource_path, settings)?;
|
||||
}
|
||||
|
||||
@@ -1076,7 +1072,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourceMap> {
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if !added_resources.iter().any(|r| r.ends_with(&relative_path)) {
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(resource_path, settings)?;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,22 +6,23 @@ use crate::{
|
||||
bundle::{
|
||||
settings::Arch,
|
||||
windows::{
|
||||
sign::{sign_command, try_sign},
|
||||
sign::{should_sign, sign_command, try_sign},
|
||||
util::{
|
||||
download_webview2_bootstrapper, download_webview2_offline_installer,
|
||||
NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME,
|
||||
},
|
||||
},
|
||||
},
|
||||
error::ErrorExt,
|
||||
utils::{
|
||||
http_utils::{download_and_verify, verify_file_hash, HashAlgorithm},
|
||||
CommandExt,
|
||||
},
|
||||
Settings,
|
||||
Error, Settings,
|
||||
};
|
||||
use tauri_utils::display_path;
|
||||
|
||||
use anyhow::Context;
|
||||
use crate::error::Context;
|
||||
use handlebars::{to_json, Handlebars};
|
||||
use tauri_utils::config::{NSISInstallerMode, NsisCompression, WebviewInstallMode};
|
||||
|
||||
@@ -35,12 +36,12 @@ 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/nsis-3.zip";
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip";
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_SHA1: &str = "057e83c7d82462ec394af76c87d06733605543d4";
|
||||
const NSIS_SHA1: &str = "EF7FF767E5CBD9EDD22ADD3A32C9B8F4500BB10D";
|
||||
const NSIS_TAURI_UTILS_URL: &str =
|
||||
"https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.1/nsis_tauri_utils.dll";
|
||||
const NSIS_TAURI_UTILS_SHA1: &str = "B053B2E5FDB97257954C8F935D80964F056520AE";
|
||||
"https://github.com/tauri-apps/nsis-tauri-utils/releases/download/nsis_tauri_utils-v0.5.2/nsis_tauri_utils.dll";
|
||||
const NSIS_TAURI_UTILS_SHA1: &str = "D0C502F45DF55C0465C9406088FF016C2E7E6817";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_REQUIRED_FILES: &[&str] = &[
|
||||
@@ -54,6 +55,9 @@ const NSIS_REQUIRED_FILES: &[&str] = &[
|
||||
"Include/x64.nsh",
|
||||
"Include/nsDialogs.nsh",
|
||||
"Include/WinMessages.nsh",
|
||||
"Include/Win/COM.nsh",
|
||||
"Include/Win/Propkey.nsh",
|
||||
"Include/Win/RestartManager.nsh",
|
||||
];
|
||||
const NSIS_PLUGIN_FILES: &[&str] = &[
|
||||
"NSISdl.dll",
|
||||
@@ -105,8 +109,9 @@ pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<P
|
||||
let data = download_and_verify(url, hash, *hash_algorithm)?;
|
||||
let out_path = nsis_toolset_path.join(path);
|
||||
std::fs::create_dir_all(out_path.parent().context("output path has no parent")?)
|
||||
.context("failed to create file output directory")?;
|
||||
fs::write(out_path, data).with_context(|| format!("failed to save {path}"))?;
|
||||
.fs_context("failed to create file output directory", out_path.clone())?;
|
||||
fs::write(&out_path, data)
|
||||
.fs_context("failed to save NSIS downloaded file", out_path.clone())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +128,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.08"), nsis_toolset_path)?;
|
||||
fs::rename(_tauri_tools_path.join("nsis-3.11"), nsis_toolset_path)?;
|
||||
}
|
||||
|
||||
// download additional plugins
|
||||
@@ -142,8 +147,9 @@ fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> c
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_add_numeric_build_number(version_str: &str) -> anyhow::Result<String> {
|
||||
let version = semver::Version::parse(version_str).context("invalid app version")?;
|
||||
fn try_add_numeric_build_number(version_str: &str) -> crate::Result<String> {
|
||||
let version = semver::Version::parse(version_str)
|
||||
.map_err(|error| Error::GenericError(format!("invalid app version: {error}")))?;
|
||||
if !version.build.is_empty() {
|
||||
let build = version.build.parse::<u64>();
|
||||
if build.is_ok() {
|
||||
@@ -192,38 +198,46 @@ fn build_nsis_app_installer(
|
||||
|
||||
// we make a copy of the NSIS directory if we're going to sign its DLLs
|
||||
// because we don't want to change the DLL hashes so the cache can reuse it
|
||||
let maybe_plugin_copy_path = if settings.can_sign() {
|
||||
let maybe_plugin_copy_path = if settings.windows().can_sign() {
|
||||
// find nsis path
|
||||
#[cfg(target_os = "linux")]
|
||||
let system_nsis_toolset_path = std::env::var_os("NSIS_PATH")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/usr/share/nsis"));
|
||||
#[cfg(target_os = "macos")]
|
||||
let system_nsis_toolset_path = std::env::var_os("NSIS_PATH")
|
||||
.map(PathBuf::from)
|
||||
.ok_or_else(|| anyhow::anyhow!("failed to resolve NSIS path"))
|
||||
.or_else(|_| {
|
||||
let mut makensis_path =
|
||||
which::which("makensis").context("failed to resolve `makensis`; did you install nsis? See https://tauri.app/distribute/windows-installer/#install-nsis for more information")?;
|
||||
// homebrew installs it as a symlink
|
||||
if makensis_path.is_symlink() {
|
||||
// read_link might return a path relative to makensis_path so we must use join() and canonicalize
|
||||
makensis_path = makensis_path
|
||||
.parent()
|
||||
.context("missing makensis parent")?
|
||||
.join(std::fs::read_link(&makensis_path).context("failed to resolve makensis symlink")?)
|
||||
.canonicalize()
|
||||
.context("failed to resolve makensis path")?;
|
||||
}
|
||||
// file structure:
|
||||
// ├── bin
|
||||
// │ ├── makensis
|
||||
// ├── share
|
||||
// │ ├── nsis
|
||||
let bin_folder = makensis_path.parent().context("missing makensis parent")?;
|
||||
let root_folder = bin_folder.parent().context("missing makensis root")?;
|
||||
crate::Result::Ok(root_folder.join("share").join("nsis"))
|
||||
let system_nsis_toolset_path = std::env::var_os("NSIS_PATH")
|
||||
.map(PathBuf::from)
|
||||
.context("failed to resolve NSIS path")
|
||||
.or_else(|_| {
|
||||
let mut makensis_path = which::which("makensis").map_err(|error| Error::CommandFailed {
|
||||
command: "makensis".to_string(),
|
||||
error: std::io::Error::other(format!("failed to find makensis: {error}")),
|
||||
})?;
|
||||
// homebrew installs it as a symlink
|
||||
if makensis_path.is_symlink() {
|
||||
// read_link might return a path relative to makensis_path so we must use join() and canonicalize
|
||||
makensis_path = makensis_path
|
||||
.parent()
|
||||
.context("missing makensis parent")?
|
||||
.join(
|
||||
std::fs::read_link(&makensis_path)
|
||||
.fs_context("failed to resolve makensis symlink", makensis_path.clone())?,
|
||||
)
|
||||
.canonicalize()
|
||||
.fs_context(
|
||||
"failed to canonicalize makensis path",
|
||||
makensis_path.clone(),
|
||||
)?;
|
||||
}
|
||||
// file structure:
|
||||
// ├── bin
|
||||
// │ ├── makensis
|
||||
// ├── share
|
||||
// │ ├── nsis
|
||||
let bin_folder = makensis_path.parent().context("missing makensis parent")?;
|
||||
let root_folder = bin_folder.parent().context("missing makensis root")?;
|
||||
crate::Result::Ok(root_folder.join("share").join("nsis"))
|
||||
})?;
|
||||
#[cfg(windows)]
|
||||
let system_nsis_toolset_path = nsis_toolset_path.to_path_buf();
|
||||
|
||||
@@ -283,7 +297,7 @@ fn build_nsis_app_installer(
|
||||
);
|
||||
data.insert("copyright", to_json(settings.copyright_string()));
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
let sign_cmd = format!("{:?}", sign_command("%1", &settings.sign_params())?);
|
||||
data.insert("uninstaller_sign_cmd", to_json(sign_cmd));
|
||||
}
|
||||
@@ -484,7 +498,9 @@ fn build_nsis_app_installer(
|
||||
.iter()
|
||||
.flat_map(|p| &p.schemes)
|
||||
.collect::<Vec<_>>();
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
if !schemes.is_empty() {
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
}
|
||||
}
|
||||
|
||||
let silent_webview2_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
|
||||
@@ -600,7 +616,7 @@ fn build_nsis_app_installer(
|
||||
));
|
||||
fs::create_dir_all(nsis_installer_path.parent().unwrap())?;
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
log::info!("Signing NSIS plugins");
|
||||
for dll in NSIS_PLUGIN_FILES {
|
||||
let path = additional_plugins_path.join(dll);
|
||||
@@ -636,11 +652,14 @@ fn build_nsis_app_installer(
|
||||
.env_remove("NSISCONFDIR")
|
||||
.current_dir(output_path)
|
||||
.piped()
|
||||
.context("error running makensis.exe")?;
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "makensis.exe".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
fs::rename(nsis_output_path, &nsis_installer_path)?;
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&nsis_installer_path, settings)?;
|
||||
} else {
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
@@ -718,7 +737,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
|
||||
let loader_path =
|
||||
dunce::simplified(&settings.project_out_directory().join("WebView2Loader.dll")).to_path_buf();
|
||||
if loader_path.exists() {
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&loader_path, settings)?;
|
||||
}
|
||||
added_resources.push(loader_path.clone());
|
||||
@@ -743,7 +762,7 @@ fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
|
||||
}
|
||||
added_resources.push(resource_path.clone());
|
||||
|
||||
if settings.can_sign() {
|
||||
if settings.windows().can_sign() && should_sign(&resource_path)? {
|
||||
try_sign(&resource_path, settings)?;
|
||||
}
|
||||
|
||||
@@ -808,7 +827,11 @@ fn generate_estimated_size(
|
||||
.chain(resources.keys())
|
||||
{
|
||||
size += std::fs::metadata(k)
|
||||
.with_context(|| format!("when getting size of {}", k.display()))?
|
||||
.map_err(|error| Error::Fs {
|
||||
context: "when getting size of",
|
||||
path: k.to_path_buf(),
|
||||
error,
|
||||
})?
|
||||
.len();
|
||||
}
|
||||
Ok(size / 1024)
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
Pop $R0
|
||||
Sleep 500
|
||||
${If} $R0 = 0
|
||||
${OrIf} $R0 = 2
|
||||
Goto app_check_done_${UniqueID}
|
||||
${Else}
|
||||
IfSilent silent_${UniqueID} ui_${UniqueID}
|
||||
|
||||
@@ -14,10 +14,6 @@ use std::sync::OnceLock;
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
impl Settings {
|
||||
pub(crate) fn can_sign(&self) -> bool {
|
||||
self.windows().sign_command.is_some() || self.windows().certificate_thumbprint.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn sign_params(&self) -> SignParams {
|
||||
SignParams {
|
||||
product_name: self.product_name().into(),
|
||||
@@ -214,7 +210,7 @@ pub fn sign_custom<P: AsRef<Path>>(
|
||||
let output = cmd.output_ok()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
|
||||
log::info!("{:?}", stdout);
|
||||
log::info!(action = "Signing";"Output of signing command:\n{}", stdout.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -233,7 +229,7 @@ pub fn sign_default<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Resu
|
||||
let output = cmd.output_ok()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
|
||||
log::info!("{:?}", stdout);
|
||||
log::info!(action = "Signing";"Output of signing command:\n{}", stdout.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -251,9 +247,39 @@ pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
pub fn try_sign<P: AsRef<Path>>(file_path: P, settings: &Settings) -> crate::Result<()> {
|
||||
if settings.can_sign() {
|
||||
if settings.no_sign() {
|
||||
log::warn!(
|
||||
"Skipping signing for {} due to --no-sign flag.",
|
||||
tauri_utils::display_path(file_path.as_ref())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
if settings.windows().can_sign() {
|
||||
log::info!(action = "Signing"; "{}", tauri_utils::display_path(file_path.as_ref()));
|
||||
sign(file_path, &settings.sign_params())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If the file is signable (is a binary file) and not signed already
|
||||
/// (will skip the verification if not on Windows since we can't verify it)
|
||||
pub fn should_sign(file_path: &Path) -> crate::Result<bool> {
|
||||
let is_binary = file_path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str())
|
||||
.is_some_and(|extension| matches!(extension, "exe" | "dll"));
|
||||
if !is_binary {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let already_signed = verify(file_path)?;
|
||||
Ok(!already_signed)
|
||||
}
|
||||
// Skip verification if not on Windows since we can't verify it
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::{
|
||||
};
|
||||
use ureq::ResponseExt;
|
||||
|
||||
use crate::utils::http_utils::download;
|
||||
use crate::utils::http_utils::{base_ureq_agent, download};
|
||||
|
||||
pub const WEBVIEW2_BOOTSTRAPPER_URL: &str = "https://go.microsoft.com/fwlink/p/?LinkId=2124703";
|
||||
pub const WEBVIEW2_OFFLINE_INSTALLER_X86_URL: &str =
|
||||
@@ -23,24 +23,18 @@ pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi";
|
||||
pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater";
|
||||
|
||||
pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> {
|
||||
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||
.proxy(ureq::Proxy::try_from_env())
|
||||
.build()
|
||||
.into();
|
||||
let agent = base_ureq_agent();
|
||||
let response = agent.head(url).call().map_err(Box::new)?;
|
||||
let final_url = response.get_uri().to_string();
|
||||
let remaining_url = final_url.strip_prefix(WEBVIEW2_URL_PREFIX).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"WebView2 URL prefix mismatch. Expected `{}`, found `{}`.",
|
||||
WEBVIEW2_URL_PREFIX,
|
||||
final_url
|
||||
)
|
||||
crate::Error::GenericError(format!(
|
||||
"WebView2 URL prefix mismatch. Expected `{WEBVIEW2_URL_PREFIX}`, found `{final_url}`."
|
||||
))
|
||||
})?;
|
||||
let (guid, filename) = remaining_url.split_once('/').ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"WebView2 URL format mismatch. Expected `<GUID>/<FILENAME>`, found `{}`.",
|
||||
remaining_url
|
||||
)
|
||||
crate::Error::GenericError(format!(
|
||||
"WebView2 URL format mismatch. Expected `<GUID>/<FILENAME>`, found `{remaining_url}`."
|
||||
))
|
||||
})?;
|
||||
Ok((guid.into(), filename.into()))
|
||||
}
|
||||
@@ -85,8 +79,7 @@ pub fn os_bitness<'a>() -> Option<&'a str> {
|
||||
}
|
||||
|
||||
pub fn patch_binary(binary_path: &PathBuf, package_type: &crate::PackageType) -> crate::Result<()> {
|
||||
let file_data = std::fs::read(binary_path)?;
|
||||
let mut file_data = file_data; // make mutable
|
||||
let mut file_data = std::fs::read(binary_path)?;
|
||||
|
||||
let pe = match goblin::Object::parse(&file_data)? {
|
||||
goblin::Object::PE(pe) => pe,
|
||||
@@ -104,17 +97,16 @@ pub fn patch_binary(binary_path: &PathBuf, package_type: &crate::PackageType) ->
|
||||
.ok_or(crate::Error::MissingBundleTypeVar)?;
|
||||
|
||||
let data_offset = tauri_bundle_section.pointer_to_raw_data as usize;
|
||||
|
||||
if data_offset + 8 > file_data.len() {
|
||||
return Err(crate::Error::BinaryOffsetOutOfRange);
|
||||
}
|
||||
|
||||
let ptr_bytes = &file_data[data_offset..data_offset + 8];
|
||||
let ptr_value = u64::from_le_bytes(ptr_bytes.try_into().map_err(|_| {
|
||||
crate::Error::BinaryParseError(
|
||||
std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid pointer bytes").into(),
|
||||
)
|
||||
})?);
|
||||
let pointer_size = if pe.is_64 { 8 } else { 4 };
|
||||
let ptr_bytes = file_data
|
||||
.get(data_offset..data_offset + pointer_size)
|
||||
.ok_or(crate::Error::BinaryOffsetOutOfRange)?;
|
||||
// `try_into` is safe to `unwrap` here because we have already checked the slice's size through `get`
|
||||
let ptr_value = if pe.is_64 {
|
||||
u64::from_le_bytes(ptr_bytes.try_into().unwrap())
|
||||
} else {
|
||||
u32::from_le_bytes(ptr_bytes.try_into().unwrap()).into()
|
||||
};
|
||||
|
||||
let rdata_section = pe
|
||||
.sections
|
||||
@@ -132,15 +124,15 @@ pub fn patch_binary(binary_path: &PathBuf, package_type: &crate::PackageType) ->
|
||||
)
|
||||
})?;
|
||||
|
||||
// see "Relative virtual address (RVA)" for explanation of offset arithmetic here:
|
||||
// https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#general-concepts
|
||||
let file_offset = rdata_section.pointer_to_raw_data as usize
|
||||
+ (rva as usize).saturating_sub(rdata_section.virtual_address as usize);
|
||||
|
||||
if file_offset + 3 > file_data.len() {
|
||||
return Err(crate::Error::BinaryOffsetOutOfRange);
|
||||
}
|
||||
|
||||
// Overwrite the string at that offset
|
||||
let string_bytes = &mut file_data[file_offset..file_offset + 3];
|
||||
let string_bytes = file_data
|
||||
.get_mut(file_offset..file_offset + 3)
|
||||
.ok_or(crate::Error::BinaryOffsetOutOfRange)?;
|
||||
match package_type {
|
||||
crate::PackageType::Nsis => string_bytes.copy_from_slice(b"NSS"),
|
||||
crate::PackageType::WindowsMsi => string_bytes.copy_from_slice(b"MSI"),
|
||||
|
||||
@@ -3,17 +3,45 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{io, num, path};
|
||||
use std::{
|
||||
fmt::Display,
|
||||
io, num,
|
||||
path::{self, PathBuf},
|
||||
};
|
||||
use thiserror::Error as DeriveError;
|
||||
|
||||
/// Errors returned by the bundler.
|
||||
#[derive(Debug, DeriveError)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// Error with context. Created by the [`Context`] trait.
|
||||
#[error("{0}: {1}")]
|
||||
Context(String, Box<Self>),
|
||||
/// File system error.
|
||||
#[error("{context} {path}: {error}")]
|
||||
Fs {
|
||||
/// Context of the error.
|
||||
context: &'static str,
|
||||
/// Path that was accessed.
|
||||
path: PathBuf,
|
||||
/// Error that occurred.
|
||||
error: io::Error,
|
||||
},
|
||||
/// Child process error.
|
||||
#[error("failed to run command {command}: {error}")]
|
||||
CommandFailed {
|
||||
/// Command that failed.
|
||||
command: String,
|
||||
/// Error that occurred.
|
||||
error: io::Error,
|
||||
},
|
||||
/// Error running tauri_utils API.
|
||||
#[error("{0}")]
|
||||
Resource(#[from] tauri_utils::Error),
|
||||
/// Bundler error.
|
||||
///
|
||||
/// This variant is no longer used as this crate no longer uses anyhow.
|
||||
// TODO(v3): remove this variant
|
||||
#[error("{0:#}")]
|
||||
BundlerError(#[from] anyhow::Error),
|
||||
/// I/O error.
|
||||
@@ -71,7 +99,14 @@ pub enum Error {
|
||||
#[error("Wrong package type {0} for platform {1}")]
|
||||
InvalidPackageType(String, String),
|
||||
/// Bundle type symbol missing in binary
|
||||
#[error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date")]
|
||||
#[cfg_attr(
|
||||
target_os = "linux",
|
||||
error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date and that symbol stripping is disabled (https://doc.rust-lang.org/cargo/reference/profiles.html#strip)")
|
||||
)]
|
||||
#[cfg_attr(
|
||||
not(target_os = "linux"),
|
||||
error("__TAURI_BUNDLE_TYPE variable not found in binary. Make sure tauri crate and tauri-cli are up to date")
|
||||
)]
|
||||
MissingBundleTypeVar,
|
||||
/// Failed to write binary file changed
|
||||
#[error("Failed to write binary file changes: `{0}`")]
|
||||
@@ -133,7 +168,110 @@ pub enum Error {
|
||||
#[cfg(target_os = "linux")]
|
||||
#[error("{0}")]
|
||||
RpmError(#[from] rpm::Error),
|
||||
/// Failed to notarize application.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("failed to notarize app: {0}")]
|
||||
AppleNotarization(#[from] NotarizeAuthError),
|
||||
/// Failed to codesign application.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("failed codesign application: {0}")]
|
||||
AppleCodesign(#[from] Box<tauri_macos_sign::Error>),
|
||||
/// Handlebars template error.
|
||||
#[error(transparent)]
|
||||
Template(#[from] handlebars::TemplateError),
|
||||
/// Semver error.
|
||||
#[error("`{0}`")]
|
||||
SemverError(#[from] semver::Error),
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum NotarizeAuthError {
|
||||
#[error(
|
||||
"The team ID is now required for notarization with app-specific password as authentication. Please set the `APPLE_TEAM_ID` environment variable. You can find the team ID in https://developer.apple.com/account#MembershipDetailsCard."
|
||||
)]
|
||||
MissingTeamId,
|
||||
#[error("could not find API key file. Please set the APPLE_API_KEY_PATH environment variables to the path to the {file_name} file")]
|
||||
MissingApiKey { file_name: String },
|
||||
#[error("no APPLE_ID & APPLE_PASSWORD & APPLE_TEAM_ID or APPLE_API_KEY & APPLE_API_ISSUER & APPLE_API_KEY_PATH environment variables found")]
|
||||
MissingCredentials,
|
||||
}
|
||||
|
||||
/// Convenient type alias of Result type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub trait Context<T> {
|
||||
// Required methods
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T> Context<T> for Result<T> {
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| Error::Context(context.to_string(), Box::new(e)))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| Error::Context(f().to_string(), Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Context<T> for Option<T> {
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.ok_or_else(|| Error::GenericError(context.to_string()))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.ok_or_else(|| Error::GenericError(f().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ErrorExt<T> {
|
||||
fn fs_context(self, context: &'static str, path: impl Into<PathBuf>) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> ErrorExt<T> for std::result::Result<T, std::io::Error> {
|
||||
fn fs_context(self, context: &'static str, path: impl Into<PathBuf>) -> Result<T> {
|
||||
self.map_err(|error| Error::Fs {
|
||||
context,
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
macro_rules! bail {
|
||||
($msg:literal $(,)?) => {
|
||||
return Err(crate::Error::GenericError($msg.into()))
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
return Err(crate::Error::GenericError($err))
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
return Err(crate::Error::GenericError(format!($fmt, $($arg)*)))
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) use bail;
|
||||
|
||||
@@ -14,6 +14,8 @@ use sha2::Digest;
|
||||
use url::Url;
|
||||
use zip::ZipArchive;
|
||||
|
||||
const BUNDLER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
fn generate_github_mirror_url_from_template(github_url: &str) -> Option<String> {
|
||||
std::env::var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE")
|
||||
.ok()
|
||||
@@ -47,17 +49,37 @@ fn generate_github_alternative_url(url: &str) -> Option<(ureq::Agent, String)> {
|
||||
|
||||
generate_github_mirror_url_from_template(url)
|
||||
.or_else(|| generate_github_mirror_url_from_base(url))
|
||||
.map(|alt_url| (ureq::agent(), alt_url))
|
||||
.map(|alt_url| {
|
||||
(
|
||||
ureq::Agent::config_builder()
|
||||
.user_agent(BUNDLER_USER_AGENT)
|
||||
.build()
|
||||
.into(),
|
||||
alt_url,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn create_agent_and_url(url: &str) -> (ureq::Agent, String) {
|
||||
generate_github_alternative_url(url).unwrap_or((
|
||||
ureq::Agent::config_builder()
|
||||
.proxy(ureq::Proxy::try_from_env())
|
||||
.build()
|
||||
.into(),
|
||||
url.to_owned(),
|
||||
))
|
||||
generate_github_alternative_url(url).unwrap_or((base_ureq_agent(), url.to_owned()))
|
||||
}
|
||||
|
||||
pub(crate) fn base_ureq_agent() -> ureq::Agent {
|
||||
#[allow(unused_mut)]
|
||||
let mut config_builder = ureq::Agent::config_builder()
|
||||
.user_agent(BUNDLER_USER_AGENT)
|
||||
.proxy(ureq::Proxy::try_from_env());
|
||||
|
||||
#[cfg(feature = "platform-certs")]
|
||||
{
|
||||
config_builder = config_builder.tls_config(
|
||||
ureq::tls::TlsConfig::builder()
|
||||
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
config_builder.build().into()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
||||
@@ -1,5 +1,141 @@
|
||||
# Changelog
|
||||
|
||||
## \[2.9.4]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`b586ecf1f`](https://www.github.com/tauri-apps/tauri/commit/b586ecf1f4b3b087f9aa6c4668c2c18b1b7925f4) ([#14416](https://www.github.com/tauri-apps/tauri/pull/14416) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Premultiply Alpha before Resizing which gets rid of the gray fringe around the icons for svg images.
|
||||
|
||||
## \[2.9.3]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`22edc65aa`](https://www.github.com/tauri-apps/tauri/commit/22edc65aad0b3e45515008e8e0866112da70c8a1) ([#14408](https://www.github.com/tauri-apps/tauri/pull/14408) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Set user-agent in bundler and cli http requests when fetching build tools.
|
||||
- [`779612ac8`](https://www.github.com/tauri-apps/tauri/commit/779612ac8425a787626da4cefdb9eaf7d63bea18) ([#14379](https://www.github.com/tauri-apps/tauri/pull/14379) by [@moubctez](https://www.github.com/tauri-apps/tauri/../../moubctez)) Properly read the `required-features` field of binaries in Cargo.toml to prevent bundling issues when the features weren't enabled.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`fd8c30b4f`](https://www.github.com/tauri-apps/tauri/commit/fd8c30b4f1bca8dd7165c5c0ebe7fbfd17662153) ([#14353](https://www.github.com/tauri-apps/tauri/pull/14353) by [@ChaseKnowlden](https://www.github.com/tauri-apps/tauri/../../ChaseKnowlden)) Premultiply Alpha before Resizing which gets rid of the gray fringe around the icons.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-bundler@2.7.3`
|
||||
|
||||
## \[2.9.2]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-bundler@2.7.2`
|
||||
|
||||
## \[2.9.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-macos-sign@2.3.0`
|
||||
- Upgraded to `tauri-bundler@2.7.1`
|
||||
|
||||
## \[2.9.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`f5851ee00`](https://www.github.com/tauri-apps/tauri/commit/f5851ee00d6d1f4d560a220ca5a728fedd525092) ([#14089](https://www.github.com/tauri-apps/tauri/pull/14089)) Adds the `scrollBarStyle` option to the window configuration.
|
||||
- [`2a06d1006`](https://www.github.com/tauri-apps/tauri/commit/2a06d10066a806e392efe8bfb16d943ee0b0b61d) ([#14052](https://www.github.com/tauri-apps/tauri/pull/14052)) Add a `--no-sign` flag to the `tauri build` and `tauri bundle` commands to skip the code signing step, improving the developer experience for local testing and development without requiring code signing keys.
|
||||
- [`3b4fac201`](https://www.github.com/tauri-apps/tauri/commit/3b4fac2017832d426dd07c5e24e26684eda57f7b) ([#14194](https://www.github.com/tauri-apps/tauri/pull/14194)) Add `tauri.conf.json > bundle > android > autoIncrementVersionCode` config option to automatically increment the Android version code.
|
||||
- [`673867aa0`](https://www.github.com/tauri-apps/tauri/commit/673867aa0e1ccd766ee879ffe96aba58c758613c) ([#14094](https://www.github.com/tauri-apps/tauri/pull/14094)) Try to detect ANDROID_HOME and NDK_HOME environment variables from default system locations and install them if needed using the Android Studio command line tools.
|
||||
- [`3d6868d09`](https://www.github.com/tauri-apps/tauri/commit/3d6868d09c323d68a152f3c3f8c7256311bd020a) ([#14128](https://www.github.com/tauri-apps/tauri/pull/14128)) Added support to defining the content type of the declared file association on macOS (maps to LSItemContentTypes property).
|
||||
- [`3d6868d09`](https://www.github.com/tauri-apps/tauri/commit/3d6868d09c323d68a152f3c3f8c7256311bd020a) ([#14128](https://www.github.com/tauri-apps/tauri/pull/14128)) Added support to defining the metadata for custom types declared in `tauri.conf.json > bundle > fileAssociations > exportedType` via the `UTExportedTypeDeclarations` Info.plist property.
|
||||
- [`ed7c9a410`](https://www.github.com/tauri-apps/tauri/commit/ed7c9a4100e08c002212265549d12130d021ad1e) ([#14108](https://www.github.com/tauri-apps/tauri/pull/14108)) Added `bundle > macOS > infoPlist` and `bundle > iOS > infoPlist` configurations to allow defining custom Info.plist extensions.
|
||||
- [`75082cc5b`](https://www.github.com/tauri-apps/tauri/commit/75082cc5b340e30e2c4b4cd4bd6a1fe5382164aa) ([#14120](https://www.github.com/tauri-apps/tauri/pull/14120)) Added `ios run` and `android run` commands to run the app in production mode.
|
||||
- [`cc8c0b531`](https://www.github.com/tauri-apps/tauri/commit/cc8c0b53171173dbd1d01781a50de1a3ea159031) ([#14031](https://www.github.com/tauri-apps/tauri/pull/14031)) Added support to universal app links on macOS with the `plugins > deep-link > desktop > domains` configuration.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`94cbd40fc`](https://www.github.com/tauri-apps/tauri/commit/94cbd40fc733e08c0bccd48149d22a0e9c2f1e5c) ([#14223](https://www.github.com/tauri-apps/tauri/pull/14223)) Add support for Android's adaptive and themed icons.
|
||||
- [`b5aa01870`](https://www.github.com/tauri-apps/tauri/commit/b5aa018702bf45dc98297698f9b7d238705865a6) ([#14268](https://www.github.com/tauri-apps/tauri/pull/14268)) Update cargo-mobile2 to 0.21, enhancing error messages and opening Xcode when multiple apps are installed.
|
||||
- [`55453e845`](https://www.github.com/tauri-apps/tauri/commit/55453e8453d927b8197f1ba9f26fd944482938f7) ([#14262](https://www.github.com/tauri-apps/tauri/pull/14262)) Check mismatched versions in `tauri info`
|
||||
- [`1a6627ee7`](https://www.github.com/tauri-apps/tauri/commit/1a6627ee7d085a4e66784e2705254714d68c7244) ([#14122](https://www.github.com/tauri-apps/tauri/pull/14122)) Set a default log level filter when running `tauri add log`.
|
||||
- [`b06b3bd09`](https://www.github.com/tauri-apps/tauri/commit/b06b3bd091b0fed26cdcfb23cacb0462a7a9cc2d) ([#14126](https://www.github.com/tauri-apps/tauri/pull/14126)) Improve error messages with more context.
|
||||
- [`f6622a3e3`](https://www.github.com/tauri-apps/tauri/commit/f6622a3e342f5dd5fb3cf6e0f79fb309a10e9b3d) ([#14129](https://www.github.com/tauri-apps/tauri/pull/14129)) Prompt to install the iOS platform if it isn't installed yet.
|
||||
- [`6bbb530fd`](https://www.github.com/tauri-apps/tauri/commit/6bbb530fd5edfc07b180a4f3782b8566872ca3b1) ([#14105](https://www.github.com/tauri-apps/tauri/pull/14105)) Warn if productName is empty when initializing mobile project.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`19fb6f7cb`](https://www.github.com/tauri-apps/tauri/commit/19fb6f7cb0d702cb2f25f6f2d1e11014d9dada5d) ([#14146](https://www.github.com/tauri-apps/tauri/pull/14146)) Strip Windows-only extensions from the binary path so an Android project initialized on Windows can be used on UNIX systems.
|
||||
- [`19fb6f7cb`](https://www.github.com/tauri-apps/tauri/commit/19fb6f7cb0d702cb2f25f6f2d1e11014d9dada5d) ([#14146](https://www.github.com/tauri-apps/tauri/pull/14146)) Enhance Android build script usage on Windows by attempting to run cmd, bat and exe formats.
|
||||
- [`28a2f9bc5`](https://www.github.com/tauri-apps/tauri/commit/28a2f9bc55f658eb71ef1a970ff9f791346f7682) ([#14101](https://www.github.com/tauri-apps/tauri/pull/14101)) Fix iOS CLI usage after modifying the package name.
|
||||
- [`d2938486e`](https://www.github.com/tauri-apps/tauri/commit/d2938486e9d974debd90c15d7160b8a17bf4d763) ([#14261](https://www.github.com/tauri-apps/tauri/pull/14261)) Replaced the non-standard nerd font character with ` ⱼₛ ` in `tarui info`
|
||||
- [`25e920e16`](https://www.github.com/tauri-apps/tauri/commit/25e920e169db900ca4f07c2bb9eb290e9f9f2c7d) ([#14298](https://www.github.com/tauri-apps/tauri/pull/14298)) Wait for dev server to exit before exiting the CLI when the app is closed on `tauri dev --no-watch`.
|
||||
- [`b0012424c`](https://www.github.com/tauri-apps/tauri/commit/b0012424c5f432debfa42ba145e2672966d5f6d5) ([#14115](https://www.github.com/tauri-apps/tauri/pull/14115)) Resolve local IP address when `tauri.conf.json > build > devUrl` host is `0.0.0.0`.
|
||||
- [`abf7e8850`](https://www.github.com/tauri-apps/tauri/commit/abf7e8850ba41e7173e9e9a3fdd6dfb8f357d72d) ([#14118](https://www.github.com/tauri-apps/tauri/pull/14118)) Fixes mobile project initialization when using `pnpx` or `pnpm dlx`.
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.8.0`
|
||||
- Upgraded to `tauri-bundler@2.7.0`
|
||||
|
||||
## \[2.8.4]
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`f70b28529`](https://www.github.com/tauri-apps/tauri/commit/f70b28529d226a2dec2f41709d8934f8f5adab25) ([#14093](https://www.github.com/tauri-apps/tauri/pull/14093) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Ensure Rust targets for mobile are installed when running the dev and build commands (previously only checked on init).
|
||||
- [`a9b342125`](https://www.github.com/tauri-apps/tauri/commit/a9b342125d5ac1bc9a4b2e8b5f73e8ca3cbcb8b2) ([#14114](https://www.github.com/tauri-apps/tauri/pull/14114) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix iOS dev and build targeting the simulator on Intel machines.
|
||||
- [`61b9b681e`](https://www.github.com/tauri-apps/tauri/commit/61b9b681e88067a53b79d2318ae005dc25addcd6) ([#14111](https://www.github.com/tauri-apps/tauri/pull/14111) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Retain `RUST_*` environment variables when running the mobile commands.
|
||||
- [`c23bec62d`](https://www.github.com/tauri-apps/tauri/commit/c23bec62d6d5724798869681aa1534423aae28e2) ([#14083](https://www.github.com/tauri-apps/tauri/pull/14083) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Tauri now ignores `macOS.minimumSystemVersion` in `tauri dev` to prevent forced rebuilds of macOS specific dependencies when using something like `rust-analyzer` at the same time as `tauri dev`.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`c37a29833`](https://www.github.com/tauri-apps/tauri/commit/c37a298331d6d744b15d32d55a2db83c884a3d6a) ([#14112](https://www.github.com/tauri-apps/tauri/pull/14112) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix usage with Deno failing with `ReferenceError: require is not defined`.
|
||||
- [`bcf000c0a`](https://www.github.com/tauri-apps/tauri/commit/bcf000c0a8607eedf488fb949b982f519abda43d) ([#14110](https://www.github.com/tauri-apps/tauri/pull/14110) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes running `ios` commands with `deno` crashing due to incorrect current working directory resolution.
|
||||
- [`7db7142f9`](https://www.github.com/tauri-apps/tauri/commit/7db7142f9ff7dc2f5719602e199b77129ceb19d3) ([#14119](https://www.github.com/tauri-apps/tauri/pull/14119) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes empty device name when using an Android emulator causing the emulator to never be detected as running.
|
||||
- [`956b4fd6f`](https://www.github.com/tauri-apps/tauri/commit/956b4fd6ffbb4312123b107ca96c87a001359b9d) ([#14106](https://www.github.com/tauri-apps/tauri/pull/14106) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Use the correct export method on Xcode < 15.4.
|
||||
|
||||
## \[2.8.3]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`0ac89d3b6`](https://www.github.com/tauri-apps/tauri/commit/0ac89d3b6c8c4a4826a4c42726e4f4a8941b3fde) ([#14078](https://www.github.com/tauri-apps/tauri/pull/14078) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Updated `cargo-mobile2` to allow running on iOS simulators that have a higher version than the XCode SDK. This fixes compatiblity issues with Apple's recent "iOS 18.5 + iOS 18.6 Simulator" platform support component.
|
||||
|
||||
## \[2.8.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-bundler@2.6.1`
|
||||
|
||||
## \[2.8.0]
|
||||
|
||||
### New Features
|
||||
|
||||
- [`91508c0b8`](https://www.github.com/tauri-apps/tauri/commit/91508c0b8d16ec61c7706e93b711c5a85aaffb4a) ([#13881](https://www.github.com/tauri-apps/tauri/pull/13881) by [@pepperoni505](https://www.github.com/tauri-apps/tauri/../../pepperoni505)) Introduces a new configuration option that allows you to specify custom folders to watch for changes when running `tauri dev`.
|
||||
- [`bc4afe7dd`](https://www.github.com/tauri-apps/tauri/commit/bc4afe7dd4780f02c2d4b1f07d97185fbc5d2bba) ([#13993](https://www.github.com/tauri-apps/tauri/pull/13993) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Check installed plugin NPM/crate versions for incompatible releases.
|
||||
- [`a9ec12843`](https://www.github.com/tauri-apps/tauri/commit/a9ec12843aa7d0eb774bd3a53e2e63da12cfa77b) ([#13521](https://www.github.com/tauri-apps/tauri/pull/13521) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Added a `--skip-stapling` option to make `tauri build|bundle` *not* wait for notarization to finish on macOS.
|
||||
- [`0c402bfb6`](https://www.github.com/tauri-apps/tauri/commit/0c402bfb6bd0bec24d928fcabe2ffef1f5cff19a) ([#13997](https://www.github.com/tauri-apps/tauri/pull/13997) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Increase default iOS deployment target iOS to 14.0.
|
||||
- [`d6d5f3707`](https://www.github.com/tauri-apps/tauri/commit/d6d5f3707768a094ff7e961ae75ba0398d772655) ([#13358](https://www.github.com/tauri-apps/tauri/pull/13358) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Added `--root-certificate-path` option to `android dev` and `ios dev` to be able to connect to HTTPS dev servers.
|
||||
|
||||
### Enhancements
|
||||
|
||||
- [`8b465a12b`](https://www.github.com/tauri-apps/tauri/commit/8b465a12ba73e94d7a3995defd9cc362d15eeebe) ([#13913](https://www.github.com/tauri-apps/tauri/pull/13913) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler now pulls the latest AppImage linuxdeploy plugin instead of using the built-in one. This should remove the libfuse requirement.
|
||||
- [`390cb9c36`](https://www.github.com/tauri-apps/tauri/commit/390cb9c36a4e2416891b64514e7ad5fc0a85ccf2) ([#13953](https://www.github.com/tauri-apps/tauri/pull/13953) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Reduced the log level of the binary patcher crate `goblin` to only show its debug logs in `-vv` and above.
|
||||
- [`4475e93e1`](https://www.github.com/tauri-apps/tauri/commit/4475e93e136e9e2bd5f3c7817fa2040924f630f6) ([#13824](https://www.github.com/tauri-apps/tauri/pull/13824) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The bundler and cli will now read TLS Certificates installed on the system when downloading tools and checking versions.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [`f0dcf9637`](https://www.github.com/tauri-apps/tauri/commit/f0dcf9637cc0d42eda05fed7dd6c5ff98bbf19ae) ([#13980](https://www.github.com/tauri-apps/tauri/pull/13980) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix the generated plugin init code of `tauri add` for `tauri-plugin-autostart` and `tauri-plugin-single-instance`
|
||||
- [`4d270a96a`](https://www.github.com/tauri-apps/tauri/commit/4d270a96a891ae83f7df751abcbe12b7072212d5) ([#13943](https://www.github.com/tauri-apps/tauri/pull/13943) by [@acx0](https://www.github.com/tauri-apps/tauri/../../acx0)) Fix codesigning verification failures caused by binary-patching during bundling
|
||||
- [`b21d86a8a`](https://www.github.com/tauri-apps/tauri/commit/b21d86a8a3ef29f16628b7d4de17ce1214e9bf49) ([#13981](https://www.github.com/tauri-apps/tauri/pull/13981) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix `tauri permission add` could add duplicated permissions to the capability files
|
||||
- [`9c938be45`](https://www.github.com/tauri-apps/tauri/commit/9c938be4520fce9204361f3b59439844bc5c91e8) ([#13912](https://www.github.com/tauri-apps/tauri/pull/13912) by [@takecchi](https://www.github.com/tauri-apps/tauri/../../takecchi)) Properly migrate svelte to v5 in the plugin example template
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-utils@2.7.0`
|
||||
- Upgraded to `tauri-bundler@2.6.0`
|
||||
- Upgraded to `tauri-macos-sign@2.2.0`
|
||||
|
||||
## \[2.7.1]
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Upgraded to `tauri-bundler@2.5.2`
|
||||
|
||||
## \[2.7.0]
|
||||
|
||||
### New Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tauri-cli"
|
||||
version = "2.7.0"
|
||||
version = "2.9.4"
|
||||
authors = ["Tauri Programme within The Commons Conservancy"]
|
||||
edition = "2021"
|
||||
rust-version = "1.77.2"
|
||||
@@ -36,7 +36,7 @@ name = "cargo-tauri"
|
||||
path = "src/main.rs"
|
||||
|
||||
[target."cfg(any(target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\", target_os = \"windows\", target_os = \"macos\"))".dependencies]
|
||||
cargo-mobile2 = { version = "0.20.2", default-features = false }
|
||||
cargo-mobile2 = { version = "0.21.1", default-features = false }
|
||||
|
||||
[dependencies]
|
||||
jsonrpsee = { version = "0.24", features = ["server"] }
|
||||
@@ -46,30 +46,31 @@ jsonrpsee-ws-client = { version = "0.24", default-features = false }
|
||||
sublime_fuzzy = "0.7"
|
||||
clap_complete = "4"
|
||||
clap = { version = "4", features = ["derive", "env"] }
|
||||
anyhow = "1"
|
||||
tauri-bundler = { version = "2.5.1", default-features = false, path = "../tauri-bundler" }
|
||||
thiserror = "2"
|
||||
tauri-bundler = { version = "2.7.3", default-features = false, path = "../tauri-bundler" }
|
||||
colored = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
json5 = "0.4"
|
||||
notify = "8"
|
||||
notify-debouncer-full = "0.5"
|
||||
notify-debouncer-full = "0.6"
|
||||
shared_child = "1"
|
||||
duct = "1.0"
|
||||
toml_edit = { version = "0.22", features = ["serde"] }
|
||||
toml_edit = { version = "0.23", features = ["serde"] }
|
||||
json-patch = "3"
|
||||
tauri-utils = { version = "2.6.0", path = "../tauri-utils", features = [
|
||||
tauri-utils = { version = "2.8.0", path = "../tauri-utils", features = [
|
||||
"isolation",
|
||||
"schema",
|
||||
"config-json5",
|
||||
"config-toml",
|
||||
"html-manipulation",
|
||||
] }
|
||||
toml = "0.8"
|
||||
jsonschema = "0.30"
|
||||
toml = "0.9"
|
||||
jsonschema = "0.33"
|
||||
handlebars = "6"
|
||||
include_dir = "0.7"
|
||||
minisign = "=0.7.3"
|
||||
dirs = "6"
|
||||
minisign = "0.8"
|
||||
base64 = "0.22"
|
||||
ureq = { version = "3", default-features = false, features = ["gzip"] }
|
||||
os_info = "3"
|
||||
@@ -110,6 +111,9 @@ memchr = "2"
|
||||
tempfile = "3"
|
||||
uuid = { version = "1", features = ["v5"] }
|
||||
rand = "0.9"
|
||||
zip = { version = "4", default-features = false, features = ["deflate"] }
|
||||
which = "8"
|
||||
rayon = "1.10"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "1"
|
||||
@@ -129,7 +133,7 @@ libc = "0.2"
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
plist = "1"
|
||||
tauri-macos-sign = { version = "2.1.0", path = "../tauri-macos-sign" }
|
||||
tauri-macos-sign = { version = "2.3.0", path = "../tauri-macos-sign" }
|
||||
object = { version = "0.36", default-features = false, features = [
|
||||
"macho",
|
||||
"read_core",
|
||||
@@ -138,7 +142,7 @@ object = { version = "0.36", default-features = false, features = [
|
||||
ar = "0.9"
|
||||
|
||||
[features]
|
||||
default = ["rustls"]
|
||||
default = ["rustls", "platform-certs"]
|
||||
native-tls = [
|
||||
"tauri-bundler/native-tls",
|
||||
"cargo-mobile2/native-tls",
|
||||
@@ -146,3 +150,4 @@ native-tls = [
|
||||
]
|
||||
native-tls-vendored = ["native-tls", "tauri-bundler/native-tls-vendored"]
|
||||
rustls = ["tauri-bundler/rustls", "cargo-mobile2/rustls", "ureq/rustls"]
|
||||
platform-certs = ["tauri-bundler/platform-certs", "ureq/platform-verifier"]
|
||||
|
||||
@@ -33,7 +33,7 @@ These environment variables are inputs to the CLI which may have an equivalent C
|
||||
- See [creating API keys](https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api) for more information.
|
||||
- `API_PRIVATE_KEYS_DIR` — Specify the directory where your AuthKey file is located. See `APPLE_API_KEY`.
|
||||
- `APPLE_API_ISSUER` — Issuer ID. Required if `APPLE_API_KEY` is specified.
|
||||
- `APPLE_API_KEY_PATH` - path to the API key `.p8` file. If not specified, for macOS apps the bundler searches the following directories in sequence for a private key file with the name of 'AuthKey\_<api_key>.p8': './private_keys', '~/private_keys', '~/.private_keys', and '~/.appstoreconnect/private_keys'. **For iOS this variable is required**.
|
||||
- `APPLE_API_KEY_PATH` - path to the API key `.p8` file. If not specified, for macOS apps the bundler searches the following directories in sequence for a private key file with the name of `AuthKey\_<api_key>.p8`: `./private_keys`, `~/private_keys`, `~/.private_keys`, and `~/.appstoreconnect/private_keys`. **For iOS this variable is required**.
|
||||
- `APPLE_SIGNING_IDENTITY` — The identity used to code sign. Overwrites `tauri.conf.json > bundle > macOS > signingIdentity`. If neither are set, it is inferred from `APPLE_CERTIFICATE` when provided.
|
||||
- `APPLE_PROVIDER_SHORT_NAME` — If your Apple ID is connected to multiple teams, you have to specify the provider short name of the team you want to use to notarize your app. Overwrites `tauri.conf.json > bundle > macOS > providerShortName`.
|
||||
- `APPLE_DEVELOPMENT_TEAM` — The team ID used to code sign on iOS. Overwrites `tauri.conf.json > bundle > iOS > developmentTeam`. Can be found in https://developer.apple.com/account#MembershipDetailsCard.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://schema.tauri.app/config/2.7.0",
|
||||
"$id": "https://schema.tauri.app/config/2.9.3",
|
||||
"title": "Config",
|
||||
"description": "The Tauri configuration object.\n It is read from a file where you can define your frontend assets,\n configure the bundler and define a tray icon.\n\n The configuration file is generated by the\n [`tauri init`](https://v2.tauri.app/reference/cli/#init) command that lives in\n your Tauri application source directory (src-tauri).\n\n Once generated, you may modify it at will to customize your Tauri application.\n\n ## File Formats\n\n By default, the configuration is defined as a JSON file named `tauri.conf.json`.\n\n Tauri also supports JSON5 and TOML files via the `config-json5` and `config-toml` Cargo features, respectively.\n The JSON5 file name must be either `tauri.conf.json` or `tauri.conf.json5`.\n The TOML file name is `Tauri.toml`.\n\n ## Platform-Specific Configuration\n\n In addition to the default configuration file, Tauri can\n read a platform-specific configuration from `tauri.linux.conf.json`,\n `tauri.windows.conf.json`, `tauri.macos.conf.json`, `tauri.android.conf.json` and `tauri.ios.conf.json`\n (or `Tauri.linux.toml`, `Tauri.windows.toml`, `Tauri.macos.toml`, `Tauri.android.toml` and `Tauri.ios.toml` if the `Tauri.toml` format is used),\n which gets merged with the main configuration object.\n\n ## Configuration Structure\n\n The configuration is composed of the following objects:\n\n - [`app`](#appconfig): The Tauri configuration\n - [`build`](#buildconfig): The build configuration\n - [`bundle`](#bundleconfig): The bundle configurations\n - [`plugins`](#pluginconfig): The plugins configuration\n\n Example tauri.config.json file:\n\n ```json\n {\n \"productName\": \"tauri-app\",\n \"version\": \"0.1.0\",\n \"build\": {\n \"beforeBuildCommand\": \"\",\n \"beforeDevCommand\": \"\",\n \"devUrl\": \"http://localhost:3000\",\n \"frontendDist\": \"../dist\"\n },\n \"app\": {\n \"security\": {\n \"csp\": null\n },\n \"windows\": [\n {\n \"fullscreen\": false,\n \"height\": 600,\n \"resizable\": true,\n \"title\": \"Tauri App\",\n \"width\": 800\n }\n ]\n },\n \"bundle\": {},\n \"plugins\": {}\n }\n ```",
|
||||
"type": "object",
|
||||
@@ -70,6 +70,7 @@
|
||||
"build": {
|
||||
"description": "The build configuration.",
|
||||
"default": {
|
||||
"additionalWatchFolders": [],
|
||||
"removeUnusedCommands": false
|
||||
},
|
||||
"allOf": [
|
||||
@@ -83,11 +84,12 @@
|
||||
"default": {
|
||||
"active": false,
|
||||
"android": {
|
||||
"autoIncrementVersionCode": false,
|
||||
"minSdkVersion": 24
|
||||
},
|
||||
"createUpdaterArtifacts": false,
|
||||
"iOS": {
|
||||
"minimumSystemVersion": "13.0"
|
||||
"minimumSystemVersion": "14.0"
|
||||
},
|
||||
"icon": [],
|
||||
"linux": {
|
||||
@@ -163,7 +165,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"windows": {
|
||||
"description": "The app windows configuration.",
|
||||
"description": "The app windows configuration.\n\n ## Example:\n\n To create a window at app startup\n\n ```json\n {\n \"app\": {\n \"windows\": [\n { \"width\": 800, \"height\": 600 }\n ]\n }\n }\n ```\n\n If not specified, the window's label (its identifier) defaults to \"main\",\n you can use this label to get the window through\n `app.get_webview_window` in Rust or `WebviewWindow.getByLabel` in JavaScript\n\n When working with multiple windows, each window will need an unique label\n\n ```json\n {\n \"app\": {\n \"windows\": [\n { \"label\": \"main\", \"width\": 800, \"height\": 600 },\n { \"label\": \"secondary\", \"width\": 800, \"height\": 600 }\n ]\n }\n }\n ```\n\n You can also set `create` to false and use this config through the Rust APIs\n\n ```json\n {\n \"app\": {\n \"windows\": [\n { \"create\": false, \"width\": 800, \"height\": 600 }\n ]\n }\n }\n ```\n\n and use it like this\n\n ```rust\n tauri::Builder::default()\n .setup(|app| {\n tauri::WebviewWindowBuilder::from_config(app.handle(), app.config().app.windows[0])?.build()?;\n Ok(())\n });\n ```",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -229,7 +231,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"create": {
|
||||
"description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).",
|
||||
"description": "Whether Tauri should create this window at app startup or not.\n\n When this is set to `false` you must manually grab the config object via `app.config().app.windows`\n and create it with [`WebviewWindowBuilder::from_config`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.from_config).\n\n ## Example:\n\n ```rust\n tauri::Builder::default()\n .setup(|app| {\n tauri::WebviewWindowBuilder::from_config(app.handle(), app.config().app.windows[0])?.build()?;\n Ok(())\n });\n ```",
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -365,6 +367,11 @@
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"focusable": {
|
||||
"description": "Whether the window will be focusable or not.",
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"transparent": {
|
||||
"description": "Whether the window is transparent or not.\n\n Note that on `macOS` this requires the `macos-private-api` feature flag, enabled under `tauri > macOSPrivateApi`.\n WARNING: Using private APIs on `macOS` prevents your application from being accepted to the `App Store`.",
|
||||
"default": false,
|
||||
@@ -489,7 +496,7 @@
|
||||
]
|
||||
},
|
||||
"incognito": {
|
||||
"description": "Whether or not the webview should be launched in incognito mode.\n\n ## Platform-specific:\n\n - **Android**: Unsupported.",
|
||||
"description": "Whether or not the webview should be launched in incognito mode.\n\n ## Platform-specific:\n\n - **Android**: Unsupported.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
@@ -566,6 +573,36 @@
|
||||
"description": "Allows disabling the input accessory view on iOS.\n\n The accessory view is the view that appears above the keyboard when a text input element is focused.\n It usually displays a view with \"Done\", \"Next\" buttons.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"dataDirectory": {
|
||||
"description": "Set a custom path for the webview's data directory (localStorage, cache, etc.) **relative to [`appDataDir()`]/${label}**.\n\n To set absolute paths, use [`WebviewWindowBuilder::data_directory`](https://docs.rs/tauri/2/tauri/webview/struct.WebviewWindowBuilder.html#method.data_directory)\n\n #### Platform-specific:\n\n - **Windows**: WebViews with different values for settings like `additionalBrowserArgs`, `browserExtensionsEnabled` or `scrollBarStyle` must have different data directories.\n - **macOS / iOS**: Unsupported, use `dataStoreIdentifier` instead.\n - **Android**: Unsupported.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dataStoreIdentifier": {
|
||||
"description": "Initialize the WebView with a custom data store identifier. This can be seen as a replacement for `dataDirectory` which is unavailable in WKWebView.\n See https://developer.apple.com/documentation/webkit/wkwebsitedatastore/init(foridentifier:)?language=objc\n\n The array must contain 16 u8 numbers.\n\n #### Platform-specific:\n\n - **iOS**: Supported since version 17.0+.\n - **macOS**: Supported since version 14.0+.\n - **Windows / Linux / Android**: Unsupported.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "integer",
|
||||
"format": "uint8",
|
||||
"minimum": 0.0
|
||||
},
|
||||
"maxItems": 16,
|
||||
"minItems": 16
|
||||
},
|
||||
"scrollBarStyle": {
|
||||
"description": "Specifies the native scrollbar style to use with the webview.\n CSS styles that modify the scrollbar are applied on top of the native appearance configured here.\n\n Defaults to `default`, which is the browser default.\n\n ## Platform-specific\n\n - **Windows**:\n - `fluentOverlay` requires WebView2 Runtime version 125.0.2535.41 or higher,\n and does nothing on older versions.\n - This option must be given the same value for all webviews that target the same data directory.\n - **Linux / Android / iOS / macOS**: Unsupported. Only supports `Default` and performs no operation.",
|
||||
"default": "default",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ScrollBarStyle"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -884,7 +921,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Mica effect that matches the system dark perefence **Windows 11 Only**",
|
||||
"description": "Mica effect that matches the system dark preference **Windows 11 Only**",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"mica"
|
||||
@@ -905,7 +942,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Tabbed effect that matches the system dark perefence **Windows 11 Only**",
|
||||
"description": "Tabbed effect that matches the system dark preference **Windows 11 Only**",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"tabbed"
|
||||
@@ -1070,14 +1107,14 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A policy where a web view that’s not in a window fully suspends tasks. This is usually the default behavior in case no policy is set.",
|
||||
"description": "A policy where a web view that's not in a window fully suspends tasks. This is usually the default behavior in case no policy is set.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"suspend"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "A policy where a web view that’s not in a window limits processing, but does not fully suspend tasks.",
|
||||
"description": "A policy where a web view that's not in a window limits processing, but does not fully suspend tasks.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"throttle"
|
||||
@@ -1085,6 +1122,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ScrollBarStyle": {
|
||||
"description": "The scrollbar style to use in the webview.\n\n ## Platform-specific\n\n - **Windows**: This option must be given the same value for all webviews that target the same data directory.",
|
||||
"oneOf": [
|
||||
{
|
||||
"description": "The scrollbar style to use in the webview.",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"default"
|
||||
]
|
||||
},
|
||||
{
|
||||
"description": "Fluent UI style overlay scrollbars. **Windows Only**\n\n Requires WebView2 Runtime version 125.0.2535.41 or higher, does nothing on older versions,\n see https://learn.microsoft.com/en-us/microsoft-edge/webview2/release-notes/?tabs=dotnetcsharp#10253541",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"fluentOverlay"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SecurityConfig": {
|
||||
"description": "Security configuration.\n\n See more: <https://v2.tauri.app/reference/config/#securityconfig>",
|
||||
"type": "object",
|
||||
@@ -1149,7 +1205,7 @@
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"description": "List of capabilities that are enabled on the application.\n\n If the list is empty, all capabilities are included.",
|
||||
"description": "List of capabilities that are enabled on the application.\n\n By default (not set or empty list), all capability files from `./capabilities/` are included,\n by setting values in this entry, you have fine grained control over which capabilities are included\n\n You can either reference a capability file defined in `./capabilities/` with its identifier or inline a [`Capability`]\n\n ### Example\n\n ```json\n {\n \"app\": {\n \"capabilities\": [\n \"main-window\",\n {\n \"identifier\": \"drag-window\",\n \"permissions\": [\"core:window:allow-start-dragging\"]\n }\n ]\n }\n }\n ```",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -1880,6 +1936,14 @@
|
||||
"description": "Try to remove unused commands registered from plugins base on the ACL list during `tauri build`,\n the way it works is that tauri-cli will read this and set the environment variables for the build script and macros,\n and they'll try to get all the allowed commands and remove the rest\n\n Note:\n - This won't be accounting for dynamically added ACLs when you use features from the `dynamic-acl` (currently enabled by default) feature flag, so make sure to check it when using this\n - This feature requires tauri-plugin 2.1 and tauri 2.4",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"additionalWatchFolders": {
|
||||
"description": "Additional paths to watch for changes when running `tauri dev`.",
|
||||
"default": [],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2057,7 +2121,7 @@
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"description": "App resources to bundle.\n Each resource is a path to a file or directory.\n Glob patterns are supported.",
|
||||
"description": "App resources to bundle.\n Each resource is a path to a file or directory.\n Glob patterns are supported.\n\n ## Examples\n\n To include a list of files:\n\n ```json\n {\n \"bundle\": {\n \"resources\": [\n \"./path/to/some-file.txt\",\n \"/absolute/path/to/textfile.txt\",\n \"../relative/path/to/jsonfile.json\",\n \"some-folder/\",\n \"resources/**/*.md\"\n ]\n }\n }\n ```\n\n The bundled files will be in `$RESOURCES/` with the original directory structure preserved,\n for example: `./path/to/some-file.txt` -> `$RESOURCE/path/to/some-file.txt`\n\n To fine control where the files will get copied to, use a map instead\n\n ```json\n {\n \"bundle\": {\n \"resources\": {\n \"/absolute/path/to/textfile.txt\": \"resources/textfile.txt\",\n \"relative/path/to/jsonfile.json\": \"resources/jsonfile.json\",\n \"resources/\": \"\",\n \"docs/**/*md\": \"website-docs/\"\n }\n }\n }\n ```\n\n Note that when using glob pattern in this case, the original directory structure is not preserved,\n everything gets copied to the target directory directly\n\n See more: <https://v2.tauri.app/develop/resources/>",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/BundleResources"
|
||||
@@ -2096,7 +2160,7 @@
|
||||
]
|
||||
},
|
||||
"fileAssociations": {
|
||||
"description": "File associations to application.",
|
||||
"description": "File types to associate with the application.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
@@ -2208,7 +2272,7 @@
|
||||
"iOS": {
|
||||
"description": "iOS configuration.",
|
||||
"default": {
|
||||
"minimumSystemVersion": "13.0"
|
||||
"minimumSystemVersion": "14.0"
|
||||
},
|
||||
"allOf": [
|
||||
{
|
||||
@@ -2219,6 +2283,7 @@
|
||||
"android": {
|
||||
"description": "Android configuration.",
|
||||
"default": {
|
||||
"autoIncrementVersionCode": false,
|
||||
"minSdkVersion": 24
|
||||
},
|
||||
"allOf": [
|
||||
@@ -2370,6 +2435,16 @@
|
||||
"$ref": "#/definitions/AssociationExt"
|
||||
}
|
||||
},
|
||||
"contentTypes": {
|
||||
"description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"name": {
|
||||
"description": "The name. Maps to `CFBundleTypeName` on macOS. Default to `ext[0]`",
|
||||
"type": [
|
||||
@@ -2408,6 +2483,17 @@
|
||||
"$ref": "#/definitions/HandlerRank"
|
||||
}
|
||||
]
|
||||
},
|
||||
"exportedType": {
|
||||
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.\n\n You should define this if the associated file is a custom file type defined by your application.",
|
||||
"anyOf": [
|
||||
{
|
||||
"$ref": "#/definitions/ExportedFileAssociation"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2489,6 +2575,30 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"ExportedFileAssociation": {
|
||||
"description": "The exported type definition. Maps to a `UTExportedTypeDeclarations` entry on macOS.",
|
||||
"type": "object",
|
||||
"required": [
|
||||
"identifier"
|
||||
],
|
||||
"properties": {
|
||||
"identifier": {
|
||||
"description": "The unique identifier for the exported type. Maps to `UTTypeIdentifier`.",
|
||||
"type": "string"
|
||||
},
|
||||
"conformsTo": {
|
||||
"description": "The types that this type conforms to. Maps to `UTTypeConformsTo`.\n\n Examples are `public.data`, `public.image`, `public.json` and `public.database`.",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"WindowsConfig": {
|
||||
"description": "Windows bundler configuration.\n\n See more: <https://v2.tauri.app/reference/config/#windowsconfig>",
|
||||
"type": "object",
|
||||
@@ -2778,6 +2888,11 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"fipsCompliant": {
|
||||
"description": "Enables FIPS compliant algorithms.\n Can also be enabled via the `TAURI_BUNDLER_WIX_FIPS_COMPLIANT` env var.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -2902,7 +3017,7 @@
|
||||
]
|
||||
},
|
||||
"installerHooks": {
|
||||
"description": "A path to a `.nsh` file that contains special NSIS macros to be hooked into the\n main installer.nsi script.\n\n Supported hooks are:\n - `NSIS_HOOK_PREINSTALL`: This hook runs before copying files, setting registry key values and creating shortcuts.\n - `NSIS_HOOK_POSTINSTALL`: This hook runs after the installer has finished copying all files, setting the registry keys and created shortcuts.\n - `NSIS_HOOK_PREUNINSTALL`: This hook runs before removing any files, registry keys and shortcuts.\n - `NSIS_HOOK_POSTUNINSTALL`: This hook runs after files, registry keys and shortcuts have been removed.\n\n\n ### Example\n\n ```nsh\n !macro NSIS_HOOK_PREINSTALL\n MessageBox MB_OK \"PreInstall\"\n !macroend\n\n !macro NSIS_HOOK_POSTINSTALL\n MessageBox MB_OK \"PostInstall\"\n !macroend\n\n !macro NSIS_HOOK_PREUNINSTALL\n MessageBox MB_OK \"PreUnInstall\"\n !macroend\n\n !macro NSIS_HOOK_POSTUNINSTALL\n MessageBox MB_OK \"PostUninstall\"\n !macroend\n\n ```",
|
||||
"description": "A path to a `.nsh` file that contains special NSIS macros to be hooked into the\n main installer.nsi script.\n\n Supported hooks are:\n\n - `NSIS_HOOK_PREINSTALL`: This hook runs before copying files, setting registry key values and creating shortcuts.\n - `NSIS_HOOK_POSTINSTALL`: This hook runs after the installer has finished copying all files, setting the registry keys and created shortcuts.\n - `NSIS_HOOK_PREUNINSTALL`: This hook runs before removing any files, registry keys and shortcuts.\n - `NSIS_HOOK_POSTUNINSTALL`: This hook runs after files, registry keys and shortcuts have been removed.\n\n ### Example\n\n ```nsh\n !macro NSIS_HOOK_PREINSTALL\n MessageBox MB_OK \"PreInstall\"\n !macroend\n\n !macro NSIS_HOOK_POSTINSTALL\n MessageBox MB_OK \"PostInstall\"\n !macroend\n\n !macro NSIS_HOOK_PREUNINSTALL\n MessageBox MB_OK \"PreUnInstall\"\n !macroend\n\n !macro NSIS_HOOK_POSTUNINSTALL\n MessageBox MB_OK \"PostUninstall\"\n !macroend\n ```",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -3464,7 +3579,7 @@
|
||||
]
|
||||
},
|
||||
"minimumSystemVersion": {
|
||||
"description": "A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.\n\n Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist`\n and the `MACOSX_DEPLOYMENT_TARGET` environment variable.\n\n An empty string is considered an invalid value so the default value is used.",
|
||||
"description": "A version string indicating the minimum macOS X version that the bundled application supports. Defaults to `10.13`.\n\n Setting it to `null` completely removes the `LSMinimumSystemVersion` field on the bundle's `Info.plist`\n and the `MACOSX_DEPLOYMENT_TARGET` environment variable.\n\n Ignored in `tauri dev`.\n\n An empty string is considered an invalid value so the default value is used.",
|
||||
"default": "10.13",
|
||||
"type": [
|
||||
"string",
|
||||
@@ -3504,6 +3619,13 @@
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"infoPlist": {
|
||||
"description": "Path to a Info.plist file to merge with the default Info.plist.\n\n Note that Tauri also looks for a `Info.plist` file in the same directory as the Tauri configuration file.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"dmg": {
|
||||
"description": "DMG-specific settings.",
|
||||
"default": {
|
||||
@@ -3673,8 +3795,15 @@
|
||||
},
|
||||
"minimumSystemVersion": {
|
||||
"description": "A version string indicating the minimum iOS version that the bundled application supports. Defaults to `13.0`.\n\n Maps to the IPHONEOS_DEPLOYMENT_TARGET value.",
|
||||
"default": "13.0",
|
||||
"default": "14.0",
|
||||
"type": "string"
|
||||
},
|
||||
"infoPlist": {
|
||||
"description": "Path to a Info.plist file to merge with the default Info.plist.\n\n Note that Tauri also looks for a `Info.plist` and `Info.ios.plist` file in the same directory as the Tauri configuration file.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
@@ -3699,6 +3828,11 @@
|
||||
"format": "uint32",
|
||||
"maximum": 2100000000.0,
|
||||
"minimum": 1.0
|
||||
},
|
||||
"autoIncrementVersionCode": {
|
||||
"description": "Whether to automatically increment the `versionCode` on each build.\n\n - If `true`, the generator will try to read the last `versionCode` from\n `tauri.properties` and increment it by 1 for every build.\n - If `false` or not set, it falls back to `version_code` or semver-derived logic.\n\n Note that to use this feature, you should remove `/tauri.properties` from `src-tauri/gen/android/app/.gitignore` so the current versionCode is committed to the repository.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"cli.js": {
|
||||
"version": "2.6.3",
|
||||
"version": "2.9.4",
|
||||
"node": ">= 10.0.0"
|
||||
},
|
||||
"tauri": "2.7.0",
|
||||
"tauri-build": "2.3.1",
|
||||
"tauri-plugin": "2.3.1"
|
||||
"tauri": "2.9.3",
|
||||
"tauri-build": "2.5.2",
|
||||
"tauri-plugin": "2.5.1"
|
||||
}
|
||||
|
||||
@@ -2330,6 +2330,14 @@
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"fipsCompliant": {
|
||||
"description": "Enables FIPS compliant algorithms.",
|
||||
"default": null,
|
||||
"type": [
|
||||
"boolean",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -9,6 +9,7 @@ use tauri_utils::acl::capability::{Capability, PermissionEntry};
|
||||
|
||||
use crate::{
|
||||
acl::FileFormat,
|
||||
error::ErrorExt,
|
||||
helpers::{app_paths::tauri_dir, prompts},
|
||||
Result,
|
||||
};
|
||||
@@ -106,7 +107,9 @@ pub fn command(options: Options) -> Result<()> {
|
||||
};
|
||||
|
||||
let path = match options.out {
|
||||
Some(o) => o.canonicalize()?,
|
||||
Some(o) => o
|
||||
.canonicalize()
|
||||
.fs_context("failed to canonicalize capability file path", o.clone())?,
|
||||
None => {
|
||||
let dir = tauri_dir();
|
||||
let capabilities_dir = dir.join("capabilities");
|
||||
@@ -125,17 +128,21 @@ pub fn command(options: Options) -> Result<()> {
|
||||
);
|
||||
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
|
||||
if overwrite {
|
||||
std::fs::remove_file(&path)?;
|
||||
std::fs::remove_file(&path).fs_context("failed to remove capability file", path.clone())?;
|
||||
} else {
|
||||
anyhow::bail!(msg);
|
||||
crate::error::bail!(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
std::fs::create_dir_all(parent).fs_context(
|
||||
"failed to create capability directory",
|
||||
parent.to_path_buf(),
|
||||
)?;
|
||||
}
|
||||
|
||||
std::fs::write(&path, options.format.serialize(&capability)?)?;
|
||||
std::fs::write(&path, options.format.serialize(&capability)?)
|
||||
.fs_context("failed to write capability file", path.clone())?;
|
||||
|
||||
log::info!(action = "Created"; "capability at {}", dunce::simplified(&path).display());
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::error::Context;
|
||||
use serde::Serialize;
|
||||
use std::fmt::Display;
|
||||
|
||||
@@ -33,8 +34,8 @@ impl FileFormat {
|
||||
|
||||
pub fn serialize<S: Serialize>(&self, s: &S) -> crate::Result<String> {
|
||||
let contents = match self {
|
||||
Self::Json => serde_json::to_string_pretty(s)?,
|
||||
Self::Toml => toml_edit::ser::to_string_pretty(s)?,
|
||||
Self::Json => serde_json::to_string_pretty(s).context("failed to serialize JSON")?,
|
||||
Self::Toml => toml_edit::ser::to_string_pretty(s).context("failed to serialize TOML")?,
|
||||
};
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::path::Path;
|
||||
use clap::Parser;
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{app_paths::resolve_tauri_dir, prompts},
|
||||
Result,
|
||||
};
|
||||
@@ -77,10 +78,32 @@ impl TomlOrJson {
|
||||
};
|
||||
}
|
||||
|
||||
fn has_permission(&self, identifier: &str) -> bool {
|
||||
(|| {
|
||||
Some(match self {
|
||||
TomlOrJson::Toml(t) => t
|
||||
.get("permissions")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some(identifier)),
|
||||
|
||||
TomlOrJson::Json(j) => j
|
||||
.as_object()?
|
||||
.get("permissions")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.any(|value| value.as_str() == Some(identifier)),
|
||||
})
|
||||
})()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn to_string(&self) -> Result<String> {
|
||||
Ok(match self {
|
||||
TomlOrJson::Toml(t) => t.to_string(),
|
||||
TomlOrJson::Json(j) => serde_json::to_string_pretty(&j)?,
|
||||
TomlOrJson::Json(j) => {
|
||||
serde_json::to_string_pretty(&j).context("failed to serialize JSON")?
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -111,12 +134,12 @@ pub struct Options {
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let dir = match resolve_tauri_dir() {
|
||||
Some(t) => t,
|
||||
None => std::env::current_dir()?,
|
||||
None => std::env::current_dir().context("failed to resolve current directory")?,
|
||||
};
|
||||
|
||||
let capabilities_dir = dir.join("capabilities");
|
||||
if !capabilities_dir.exists() {
|
||||
anyhow::bail!(
|
||||
crate::error::bail!(
|
||||
"Couldn't find capabilities directory at {}",
|
||||
dunce::simplified(&capabilities_dir).display()
|
||||
);
|
||||
@@ -128,7 +151,11 @@ pub fn command(options: Options) -> Result<()> {
|
||||
.split_once(':')
|
||||
.and_then(|(plugin, _permission)| known_plugins.get(&plugin));
|
||||
|
||||
let capabilities_iter = std::fs::read_dir(&capabilities_dir)?
|
||||
let capabilities_iter = std::fs::read_dir(&capabilities_dir)
|
||||
.fs_context(
|
||||
"failed to read capabilities directory",
|
||||
capabilities_dir.clone(),
|
||||
)?
|
||||
.flatten()
|
||||
.filter(|e| e.file_type().map(|e| e.is_file()).unwrap_or_default())
|
||||
.filter_map(|e| {
|
||||
@@ -220,7 +247,7 @@ pub fn command(options: Options) -> Result<()> {
|
||||
)?;
|
||||
|
||||
if selections.is_empty() {
|
||||
anyhow::bail!("You did not select any capabilities to update");
|
||||
crate::error::bail!("You did not select any capabilities to update");
|
||||
}
|
||||
|
||||
selections
|
||||
@@ -232,13 +259,23 @@ pub fn command(options: Options) -> Result<()> {
|
||||
};
|
||||
|
||||
if capabilities.is_empty() {
|
||||
anyhow::bail!("Could not find a capability to update");
|
||||
crate::error::bail!("Could not find a capability to update");
|
||||
}
|
||||
|
||||
for (capability, path) in &mut capabilities {
|
||||
capability.insert_permission(options.identifier.clone());
|
||||
std::fs::write(&*path, capability.to_string()?)?;
|
||||
log::info!(action = "Added"; "permission `{}` to `{}` at {}", options.identifier, capability.identifier(), dunce::simplified(path).display());
|
||||
if capability.has_permission(&options.identifier) {
|
||||
log::info!(
|
||||
"Permission `{}` already found in `{}` at {}",
|
||||
options.identifier,
|
||||
capability.identifier(),
|
||||
dunce::simplified(path).display()
|
||||
);
|
||||
} else {
|
||||
capability.insert_permission(options.identifier.clone());
|
||||
std::fs::write(&*path, capability.to_string()?)
|
||||
.fs_context("failed to write capability file", path.clone())?;
|
||||
log::info!(action = "Added"; "permission `{}` to `{}` at {}", options.identifier, capability.identifier(), dunce::simplified(path).display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -4,7 +4,11 @@
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::{helpers::app_paths::tauri_dir, Result};
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::app_paths::tauri_dir,
|
||||
Result,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use tauri_utils::acl::{manifest::Manifest, APP_ACL_KEY};
|
||||
|
||||
@@ -29,8 +33,10 @@ pub fn command(options: Options) -> Result<()> {
|
||||
.join("acl-manifests.json");
|
||||
|
||||
if acl_manifests_path.exists() {
|
||||
let plugin_manifest_json = read_to_string(&acl_manifests_path)?;
|
||||
let acl = serde_json::from_str::<BTreeMap<String, Manifest>>(&plugin_manifest_json)?;
|
||||
let plugin_manifest_json = read_to_string(&acl_manifests_path)
|
||||
.fs_context("failed to read plugin manifest", acl_manifests_path)?;
|
||||
let acl = serde_json::from_str::<BTreeMap<String, Manifest>>(&plugin_manifest_json)
|
||||
.context("failed to parse plugin manifest as JSON")?;
|
||||
|
||||
for (key, manifest) in acl {
|
||||
if options
|
||||
@@ -147,6 +153,6 @@ pub fn command(options: Options) -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
anyhow::bail!("permission file not found, please build your application once first")
|
||||
crate::error::bail!("permission file not found, please build your application once first")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use clap::Parser;
|
||||
|
||||
use crate::{
|
||||
acl::FileFormat,
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{app_paths::resolve_tauri_dir, prompts},
|
||||
Result,
|
||||
};
|
||||
@@ -67,11 +68,13 @@ pub fn command(options: Options) -> Result<()> {
|
||||
};
|
||||
|
||||
let path = match options.out {
|
||||
Some(o) => o.canonicalize()?,
|
||||
Some(o) => o
|
||||
.canonicalize()
|
||||
.fs_context("failed to canonicalize permission file path", o.clone())?,
|
||||
None => {
|
||||
let dir = match resolve_tauri_dir() {
|
||||
Some(t) => t,
|
||||
None => std::env::current_dir()?,
|
||||
None => std::env::current_dir().context("failed to resolve current directory")?,
|
||||
};
|
||||
let permissions_dir = dir.join("permissions");
|
||||
permissions_dir.join(format!(
|
||||
@@ -89,24 +92,31 @@ pub fn command(options: Options) -> Result<()> {
|
||||
);
|
||||
let overwrite = prompts::confirm(&format!("{msg}, overwrite?"), Some(false))?;
|
||||
if overwrite {
|
||||
std::fs::remove_file(&path)?;
|
||||
std::fs::remove_file(&path).fs_context("failed to remove permission file", path.clone())?;
|
||||
} else {
|
||||
anyhow::bail!(msg);
|
||||
crate::error::bail!(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
std::fs::create_dir_all(parent).fs_context(
|
||||
"failed to create permission directory",
|
||||
parent.to_path_buf(),
|
||||
)?;
|
||||
}
|
||||
|
||||
std::fs::write(
|
||||
&path,
|
||||
options.format.serialize(&PermissionFile {
|
||||
default: None,
|
||||
set: Vec::new(),
|
||||
permission: vec![permission],
|
||||
})?,
|
||||
)?;
|
||||
options
|
||||
.format
|
||||
.serialize(&PermissionFile {
|
||||
default: None,
|
||||
set: Vec::new(),
|
||||
permission: vec![permission],
|
||||
})
|
||||
.context("failed to serialize permission")?,
|
||||
)
|
||||
.fs_context("failed to write permission file", path.clone())?;
|
||||
|
||||
log::info!(action = "Created"; "permission at {}", dunce::simplified(&path).display());
|
||||
|
||||
|
||||
@@ -7,11 +7,21 @@ use std::path::Path;
|
||||
use clap::Parser;
|
||||
use tauri_utils::acl::{manifest::PermissionFile, PERMISSION_SCHEMA_FILE_NAME};
|
||||
|
||||
use crate::{acl::FileFormat, helpers::app_paths::resolve_tauri_dir, Result};
|
||||
use crate::{
|
||||
acl::FileFormat,
|
||||
error::{Context, ErrorExt},
|
||||
helpers::app_paths::resolve_tauri_dir,
|
||||
Result,
|
||||
};
|
||||
|
||||
fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> {
|
||||
for entry in std::fs::read_dir(dir)?.flatten() {
|
||||
let file_type = entry.file_type()?;
|
||||
for entry in std::fs::read_dir(dir)
|
||||
.fs_context("failed to read permissions directory", dir.to_path_buf())?
|
||||
.flatten()
|
||||
{
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.fs_context("failed to get permission file type", entry.path())?;
|
||||
let path = entry.path();
|
||||
if file_type.is_dir() {
|
||||
rm_permission_files(identifier, &path)?;
|
||||
@@ -27,12 +37,21 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> {
|
||||
let (mut permission_file, format): (PermissionFile, FileFormat) =
|
||||
match path.extension().and_then(|o| o.to_str()) {
|
||||
Some("toml") => {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
(toml::from_str(&content)?, FileFormat::Toml)
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.fs_context("failed to read permission file", path.clone())?;
|
||||
(
|
||||
toml::from_str(&content).context("failed to deserialize permission file")?,
|
||||
FileFormat::Toml,
|
||||
)
|
||||
}
|
||||
Some("json") => {
|
||||
let content = std::fs::read(&path)?;
|
||||
(serde_json::from_slice(&content)?, FileFormat::Json)
|
||||
let content =
|
||||
std::fs::read(&path).fs_context("failed to read permission file", path.clone())?;
|
||||
(
|
||||
serde_json::from_slice(&content)
|
||||
.context("failed to parse permission file as JSON")?,
|
||||
FileFormat::Json,
|
||||
)
|
||||
}
|
||||
_ => {
|
||||
continue;
|
||||
@@ -63,10 +82,16 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> {
|
||||
&& permission_file.set.is_empty()
|
||||
&& permission_file.permission.is_empty()
|
||||
{
|
||||
std::fs::remove_file(&path)?;
|
||||
std::fs::remove_file(&path).fs_context("failed to remove permission file", path.clone())?;
|
||||
log::info!(action = "Removed"; "file {}", dunce::simplified(&path).display());
|
||||
} else if updated {
|
||||
std::fs::write(&path, format.serialize(&permission_file)?)?;
|
||||
std::fs::write(
|
||||
&path,
|
||||
format
|
||||
.serialize(&permission_file)
|
||||
.context("failed to serialize permission")?,
|
||||
)
|
||||
.fs_context("failed to write permission file", path.clone())?;
|
||||
log::info!(action = "Removed"; "permission {identifier} from {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
@@ -76,13 +101,19 @@ fn rm_permission_files(identifier: &str, dir: &Path) -> Result<()> {
|
||||
}
|
||||
|
||||
fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> {
|
||||
for entry in std::fs::read_dir(dir)?.flatten() {
|
||||
let file_type = entry.file_type()?;
|
||||
for entry in std::fs::read_dir(dir)
|
||||
.fs_context("failed to read capabilities directory", dir.to_path_buf())?
|
||||
.flatten()
|
||||
{
|
||||
let file_type = entry
|
||||
.file_type()
|
||||
.fs_context("failed to get capability file type", entry.path())?;
|
||||
if file_type.is_file() {
|
||||
let path = entry.path();
|
||||
match path.extension().and_then(|o| o.to_str()) {
|
||||
Some("toml") => {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.fs_context("failed to read capability file", path.clone())?;
|
||||
if let Ok(mut value) = content.parse::<toml_edit::DocumentMut>() {
|
||||
if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) {
|
||||
let prev_len = permissions.len();
|
||||
@@ -98,14 +129,16 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> {
|
||||
_ => false,
|
||||
});
|
||||
if prev_len != permissions.len() {
|
||||
std::fs::write(&path, value.to_string())?;
|
||||
std::fs::write(&path, value.to_string())
|
||||
.fs_context("failed to write capability file", path.clone())?;
|
||||
log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some("json") => {
|
||||
let content = std::fs::read(&path)?;
|
||||
let content =
|
||||
std::fs::read(&path).fs_context("failed to read capability file", path.clone())?;
|
||||
if let Ok(mut value) = serde_json::from_slice::<serde_json::Value>(&content) {
|
||||
if let Some(permissions) = value.get_mut("permissions").and_then(|p| p.as_array_mut()) {
|
||||
let prev_len = permissions.len();
|
||||
@@ -121,7 +154,12 @@ fn rm_permission_from_capabilities(identifier: &str, dir: &Path) -> Result<()> {
|
||||
_ => false,
|
||||
});
|
||||
if prev_len != permissions.len() {
|
||||
std::fs::write(&path, serde_json::to_vec_pretty(&value)?)?;
|
||||
std::fs::write(
|
||||
&path,
|
||||
serde_json::to_vec_pretty(&value)
|
||||
.context("failed to serialize capability JSON")?,
|
||||
)
|
||||
.fs_context("failed to write capability file", path.clone())?;
|
||||
log::info!(action = "Removed"; "permission from capability at {}", dunce::simplified(&path).display());
|
||||
}
|
||||
}
|
||||
@@ -152,7 +190,9 @@ pub struct Options {
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let permissions_dir = std::env::current_dir()?.join("permissions");
|
||||
let permissions_dir = std::env::current_dir()
|
||||
.context("failed to resolve current directory")?
|
||||
.join("permissions");
|
||||
if permissions_dir.exists() {
|
||||
rm_permission_files(&options.identifier, &permissions_dir)?;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
acl,
|
||||
error::ErrorExt,
|
||||
helpers::{
|
||||
app_paths::{resolve_frontend_dir, tauri_dir},
|
||||
cargo,
|
||||
@@ -64,7 +65,7 @@ pub fn run(options: Options) -> Result<()> {
|
||||
};
|
||||
|
||||
if !is_known && (options.tag.is_some() || options.rev.is_some() || options.branch.is_some()) {
|
||||
anyhow::bail!(
|
||||
crate::error::bail!(
|
||||
"Git options --tag, --rev and --branch can only be used with official Tauri plugins"
|
||||
);
|
||||
}
|
||||
@@ -114,7 +115,7 @@ pub fn run(options: Options) -> Result<()> {
|
||||
format!("tauri-apps/tauri-plugin-{plugin}#{branch}")
|
||||
}
|
||||
(None, None, None, None) => npm_name,
|
||||
_ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
|
||||
_ => crate::error::bail!("Only one of --tag, --rev and --branch can be specified"),
|
||||
};
|
||||
manager.install(&[npm_spec], tauri_dir)?;
|
||||
}
|
||||
@@ -130,6 +131,10 @@ pub fn run(options: Options) -> Result<()> {
|
||||
"Builder::new(|pass| todo!()).build()"
|
||||
} else if plugin == "localhost" {
|
||||
"Builder::new(todo!()).build()"
|
||||
} else if plugin == "single-instance" {
|
||||
"init(|app, args, cwd| {})"
|
||||
} else if plugin == "log" {
|
||||
"Builder::new().level(tauri_plugin_log::log::LevelFilter::Info).build()"
|
||||
} else if metadata.builder {
|
||||
"Builder::new().build()"
|
||||
} else {
|
||||
@@ -137,9 +142,10 @@ pub fn run(options: Options) -> Result<()> {
|
||||
};
|
||||
let plugin_init = format!(".plugin(tauri_plugin_{plugin_snake_case}::{plugin_init_fn})");
|
||||
|
||||
let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)")?;
|
||||
let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)").unwrap();
|
||||
for file in [tauri_dir.join("src/main.rs"), tauri_dir.join("src/lib.rs")] {
|
||||
let contents = std::fs::read_to_string(&file)?;
|
||||
let contents =
|
||||
std::fs::read_to_string(&file).fs_context("failed to read Rust entry point", file.clone())?;
|
||||
|
||||
if contents.contains(&plugin_init) {
|
||||
log::info!(
|
||||
@@ -153,7 +159,7 @@ pub fn run(options: Options) -> Result<()> {
|
||||
let out = re.replace(&contents, format!("$1$2{plugin_init}$2"));
|
||||
|
||||
log::info!("Adding plugin to {}", file.display());
|
||||
std::fs::write(file, out.as_bytes())?;
|
||||
std::fs::write(&file, out.as_bytes()).fs_context("failed to write plugin init code", file)?;
|
||||
|
||||
if !options.no_fmt {
|
||||
// reformat code with rustfmt
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
|
||||
use crate::{
|
||||
bundle::BundleFormat,
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{
|
||||
self,
|
||||
app_paths::tauri_dir,
|
||||
app_paths::{frontend_dir, tauri_dir},
|
||||
config::{get as get_config, ConfigHandle, FrontendDist},
|
||||
},
|
||||
info::plugins::check_mismatched_packages,
|
||||
interface::{rust::get_cargo_target_dir, AppInterface, Interface},
|
||||
ConfigValue, Result,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use clap::{ArgAction, Parser};
|
||||
use std::env::set_current_dir;
|
||||
use tauri_utils::config::RunnerConfig;
|
||||
@@ -60,11 +61,33 @@ pub struct Options {
|
||||
/// Skip prompting for values
|
||||
#[clap(long, env = "CI")]
|
||||
pub ci: bool,
|
||||
/// Whether to wait for notarization to finish and `staple` the ticket onto the app.
|
||||
///
|
||||
/// Gatekeeper will look for stapled tickets to tell whether your app was notarized without
|
||||
/// reaching out to Apple's servers which is helpful in offline environments.
|
||||
///
|
||||
/// Enabling this option will also result in `tauri build` not waiting for notarization to finish
|
||||
/// which is helpful for the very first time your app is notarized as this can take multiple hours.
|
||||
/// On subsequent runs, it's recommended to disable this setting again.
|
||||
#[clap(long)]
|
||||
pub skip_stapling: bool,
|
||||
/// Do not error out if a version mismatch is detected on a Tauri package.
|
||||
///
|
||||
/// Only use this when you are sure the mismatch is incorrectly detected as version mismatched Tauri packages can lead to unknown behavior.
|
||||
#[clap(long)]
|
||||
pub ignore_version_mismatches: bool,
|
||||
/// Skip code signing when bundling the app
|
||||
#[clap(long)]
|
||||
pub no_sign: bool,
|
||||
}
|
||||
|
||||
pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
||||
crate::helpers::app_paths::resolve();
|
||||
|
||||
if options.no_sign {
|
||||
log::warn!("--no-sign flag detected: Signing will be skipped.");
|
||||
}
|
||||
|
||||
let ci = options.ci;
|
||||
|
||||
let target = options
|
||||
@@ -88,6 +111,10 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
|
||||
let config_guard = config.lock().unwrap();
|
||||
let config_ = config_guard.as_ref().unwrap();
|
||||
|
||||
if let Some(minimum_system_version) = &config_.bundle.macos.minimum_system_version {
|
||||
std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
|
||||
}
|
||||
|
||||
let app_settings = interface.app_settings();
|
||||
let interface_options = options.clone().into();
|
||||
|
||||
@@ -121,7 +148,19 @@ pub fn setup(
|
||||
mobile: bool,
|
||||
) -> Result<()> {
|
||||
let tauri_path = tauri_dir();
|
||||
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
|
||||
|
||||
// TODO: Maybe optimize this to run in parallel in the future
|
||||
// see https://github.com/tauri-apps/tauri/pull/13993#discussion_r2280697117
|
||||
log::info!("Looking up installed tauri packages to check mismatched versions...");
|
||||
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
|
||||
if options.ignore_version_mismatches {
|
||||
log::error!("{error}");
|
||||
} else {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
set_current_dir(tauri_path).context("failed to set current directory")?;
|
||||
|
||||
let config_guard = config.lock().unwrap();
|
||||
let config_ = config_guard.as_ref().unwrap();
|
||||
@@ -131,11 +170,9 @@ pub fn setup(
|
||||
.unwrap_or_else(|| "tauri.conf.json".into());
|
||||
|
||||
if config_.identifier == "com.tauri.dev" {
|
||||
log::error!(
|
||||
"You must change the bundle identifier in `{} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
|
||||
bundle_identifier_source
|
||||
crate::error::bail!(
|
||||
"You must change the bundle identifier in `{bundle_identifier_source} identifier`. The default value `com.tauri.dev` is not allowed as it must be unique across applications.",
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if config_
|
||||
@@ -143,12 +180,11 @@ pub fn setup(
|
||||
.chars()
|
||||
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
|
||||
{
|
||||
log::error!(
|
||||
crate::error::bail!(
|
||||
"The bundle identifier \"{}\" set in `{} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
|
||||
config_.identifier,
|
||||
bundle_identifier_source
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if config_.identifier.ends_with(".app") {
|
||||
@@ -170,15 +206,20 @@ pub fn setup(
|
||||
.and_then(|p| p.canonicalize().ok())
|
||||
.map(|p| p.join(web_asset_path.file_name().unwrap()))
|
||||
.unwrap_or_else(|| std::env::current_dir().unwrap().join(web_asset_path));
|
||||
return Err(anyhow::anyhow!(
|
||||
"Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).",
|
||||
web_asset_path.display(), absolute_path.display(),
|
||||
));
|
||||
crate::error::bail!(
|
||||
"Unable to find your web assets, did you forget to build your web app? Your frontendDist is set to \"{}\" (which is `{}`).",
|
||||
web_asset_path.display(), absolute_path.display(),
|
||||
);
|
||||
}
|
||||
if web_asset_path.canonicalize()?.file_name() == Some(std::ffi::OsStr::new("src-tauri")) {
|
||||
return Err(anyhow::anyhow!(
|
||||
if web_asset_path
|
||||
.canonicalize()
|
||||
.fs_context("failed to canonicalize path", web_asset_path.to_path_buf())?
|
||||
.file_name()
|
||||
== Some(std::ffi::OsStr::new("src-tauri"))
|
||||
{
|
||||
crate::error::bail!(
|
||||
"The configured frontendDist is the `src-tauri` folder. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
// Issue #13287 - Allow the use of target dir inside frontendDist/distDir
|
||||
@@ -202,11 +243,11 @@ pub fn setup(
|
||||
}
|
||||
|
||||
if !out_folders.is_empty() {
|
||||
return Err(anyhow::anyhow!(
|
||||
crate::error::bail!(
|
||||
"The configured frontendDist includes the `{:?}` {}. Please isolate your web assets on a separate folder and update `tauri.conf.json > build > frontendDist`.",
|
||||
out_folders,
|
||||
if out_folders.len() == 1 { "folder" } else { "folders" }
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,12 @@ use std::{
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{builder::PossibleValue, ArgAction, Parser, ValueEnum};
|
||||
use tauri_bundler::PackageType;
|
||||
use tauri_utils::platform::Target;
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{
|
||||
self,
|
||||
app_paths::tauri_dir,
|
||||
@@ -28,11 +28,11 @@ use crate::{
|
||||
pub struct BundleFormat(PackageType);
|
||||
|
||||
impl FromStr for BundleFormat {
|
||||
type Err = anyhow::Error;
|
||||
type Err = crate::Error;
|
||||
fn from_str(s: &str) -> crate::Result<Self> {
|
||||
PackageType::from_short_name(s)
|
||||
.map(Self)
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown bundle format {s}"))
|
||||
.with_context(|| format!("unknown bundle format {s}"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,24 @@ pub struct Options {
|
||||
/// Skip prompting for values
|
||||
#[clap(long, env = "CI")]
|
||||
pub ci: bool,
|
||||
/// Whether to wait for notarization to finish and `staple` the ticket onto the app.
|
||||
///
|
||||
/// Gatekeeper will look for stapled tickets to tell whether your app was notarized without
|
||||
/// reaching out to Apple's servers which is helpful in offline environments.
|
||||
///
|
||||
/// Enabling this option will also result in `tauri build` not waiting for notarization to finish
|
||||
/// which is helpful for the very first time your app is notarized as this can take multiple hours.
|
||||
/// On subsequent runs, it's recommended to disable this setting again.
|
||||
#[clap(long)]
|
||||
pub skip_stapling: bool,
|
||||
|
||||
/// Skip code signing during the build or bundling process.
|
||||
///
|
||||
/// Useful for local development and CI environments
|
||||
/// where signing certificates or environment variables
|
||||
/// are not available or not needed.
|
||||
#[clap(long)]
|
||||
pub no_sign: bool,
|
||||
}
|
||||
|
||||
impl From<crate::build::Options> for Options {
|
||||
@@ -93,6 +111,8 @@ impl From<crate::build::Options> for Options {
|
||||
debug: value.debug,
|
||||
ci: value.ci,
|
||||
config: value.config,
|
||||
skip_stapling: value.skip_stapling,
|
||||
no_sign: value.no_sign,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,12 +139,15 @@ pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
|
||||
)?;
|
||||
|
||||
let tauri_path = tauri_dir();
|
||||
std::env::set_current_dir(tauri_path)
|
||||
.with_context(|| "failed to change current working directory")?;
|
||||
std::env::set_current_dir(tauri_path).context("failed to set current directory")?;
|
||||
|
||||
let config_guard = config.lock().unwrap();
|
||||
let config_ = config_guard.as_ref().unwrap();
|
||||
|
||||
if let Some(minimum_system_version) = &config_.bundle.macos.minimum_system_version {
|
||||
std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
|
||||
}
|
||||
|
||||
let app_settings = interface.app_settings();
|
||||
let interface_options = options.clone().into();
|
||||
|
||||
@@ -182,6 +205,7 @@ pub fn bundle<A: AppSettings>(
|
||||
let mut settings = app_settings
|
||||
.get_bundler_settings(options.clone().into(), config, out_dir, package_types)
|
||||
.with_context(|| "failed to build bundler settings")?;
|
||||
settings.set_no_sign(options.no_sign);
|
||||
|
||||
settings.set_log_level(match verbosity {
|
||||
0 => log::Level::Error,
|
||||
@@ -189,12 +213,7 @@ pub fn bundle<A: AppSettings>(
|
||||
_ => log::Level::Trace,
|
||||
});
|
||||
|
||||
let bundles = tauri_bundler::bundle_project(&settings)
|
||||
.map_err(|e| match e {
|
||||
tauri_bundler::Error::BundlerError(e) => e,
|
||||
e => anyhow::anyhow!("{e:#}"),
|
||||
})
|
||||
.with_context(|| "failed to bundle project")?;
|
||||
let bundles = tauri_bundler::bundle_project(&settings).map_err(Box::new)?;
|
||||
|
||||
sign_updaters(settings, bundles, ci)?;
|
||||
|
||||
@@ -235,7 +254,8 @@ fn sign_updaters(
|
||||
// check if pubkey points to a file...
|
||||
let maybe_path = Path::new(pubkey);
|
||||
let pubkey = if maybe_path.exists() {
|
||||
std::fs::read_to_string(maybe_path)?
|
||||
std::fs::read_to_string(maybe_path)
|
||||
.fs_context("failed to read pubkey from file", maybe_path.to_path_buf())?
|
||||
} else {
|
||||
pubkey.to_string()
|
||||
};
|
||||
@@ -247,12 +267,15 @@ fn sign_updaters(
|
||||
|
||||
// get the private key
|
||||
let private_key = std::env::var("TAURI_SIGNING_PRIVATE_KEY")
|
||||
.map_err(|_| anyhow::anyhow!("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable."))?;
|
||||
.ok()
|
||||
.context("A public key has been found, but no private key. Make sure to set `TAURI_SIGNING_PRIVATE_KEY` environment variable.")?;
|
||||
// check if private_key points to a file...
|
||||
let maybe_path = Path::new(&private_key);
|
||||
let private_key = if maybe_path.exists() {
|
||||
std::fs::read_to_string(maybe_path)
|
||||
.with_context(|| format!("faild to read {}", maybe_path.display()))?
|
||||
std::fs::read_to_string(maybe_path).fs_context(
|
||||
"failed to read private key from file",
|
||||
maybe_path.to_path_buf(),
|
||||
)?
|
||||
} else {
|
||||
private_key
|
||||
};
|
||||
@@ -290,11 +313,11 @@ fn print_signed_updater_archive(output_paths: &[PathBuf]) -> crate::Result<()> {
|
||||
};
|
||||
let mut printable_paths = String::new();
|
||||
for path in output_paths {
|
||||
writeln!(
|
||||
let _ = writeln!(
|
||||
printable_paths,
|
||||
" {}",
|
||||
tauri_utils::display_path(path)
|
||||
)?;
|
||||
);
|
||||
}
|
||||
log::info!( action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use crate::{error::ErrorExt, Result};
|
||||
use clap::{Command, Parser};
|
||||
use clap_complete::{generate, Shell};
|
||||
|
||||
@@ -95,7 +94,7 @@ pub fn command(options: Options, cmd: Command) -> Result<()> {
|
||||
|
||||
let completions = get_completions(options.shell, cmd)?;
|
||||
if let Some(output) = options.output {
|
||||
write(output, completions).context("failed to write to output path")?;
|
||||
write(&output, completions).fs_context("failed to write to completions", output)?;
|
||||
} else {
|
||||
print!("{completions}");
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{
|
||||
app_paths::{frontend_dir, tauri_dir},
|
||||
command_env,
|
||||
@@ -10,11 +11,11 @@ use crate::{
|
||||
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
|
||||
},
|
||||
},
|
||||
info::plugins::check_mismatched_packages,
|
||||
interface::{AppInterface, ExitReason, Interface},
|
||||
CommandExt, ConfigValue, Result,
|
||||
CommandExt, ConfigValue, Error, Result,
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use clap::{ArgAction, Parser};
|
||||
use shared_child::SharedChild;
|
||||
use tauri_utils::{config::RunnerConfig, platform::Target};
|
||||
@@ -22,6 +23,7 @@ use tauri_utils::{config::RunnerConfig, platform::Target};
|
||||
use std::{
|
||||
env::set_current_dir,
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
path::PathBuf,
|
||||
process::{exit, Command, Stdio},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@@ -81,6 +83,9 @@ pub struct Options {
|
||||
/// Disable the file watcher.
|
||||
#[clap(long)]
|
||||
pub no_watch: bool,
|
||||
/// Additional paths to watch for changes.
|
||||
#[clap(long)]
|
||||
pub additional_watch_folders: Vec<PathBuf>,
|
||||
|
||||
/// Disable the built-in dev server for static files.
|
||||
#[clap(long)]
|
||||
@@ -131,7 +136,14 @@ fn command_internal(mut options: Options) -> Result<()> {
|
||||
|
||||
pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHandle) -> Result<()> {
|
||||
let tauri_path = tauri_dir();
|
||||
set_current_dir(tauri_path).with_context(|| "failed to change current working directory")?;
|
||||
|
||||
std::thread::spawn(|| {
|
||||
if let Err(error) = check_mismatched_packages(frontend_dir(), tauri_path) {
|
||||
log::error!("{error}");
|
||||
}
|
||||
});
|
||||
|
||||
set_current_dir(tauri_path).context("failed to set current directory")?;
|
||||
|
||||
if let Some(before_dev) = config
|
||||
.lock()
|
||||
@@ -178,15 +190,15 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
|
||||
};
|
||||
|
||||
if wait {
|
||||
let status = command.piped().with_context(|| {
|
||||
format!(
|
||||
"failed to run `{}` with `{}`",
|
||||
before_dev,
|
||||
let status = command.piped().map_err(|error| Error::CommandFailed {
|
||||
command: format!(
|
||||
"`{before_dev}` with `{}`",
|
||||
if cfg!(windows) { "cmd /S /C" } else { "sh -c" }
|
||||
)
|
||||
),
|
||||
error,
|
||||
})?;
|
||||
if !status.success() {
|
||||
bail!(
|
||||
crate::error::bail!(
|
||||
"beforeDevCommand `{}` failed with exit code {}",
|
||||
before_dev,
|
||||
status.code().unwrap_or_default()
|
||||
@@ -194,8 +206,8 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
|
||||
}
|
||||
} else {
|
||||
command.stdin(Stdio::piped());
|
||||
command.stdout(os_pipe::dup_stdout()?);
|
||||
command.stderr(os_pipe::dup_stderr()?);
|
||||
command.stdout(os_pipe::dup_stdout().unwrap());
|
||||
command.stderr(os_pipe::dup_stderr().unwrap());
|
||||
|
||||
let child = SharedChild::spawn(&mut command)
|
||||
.unwrap_or_else(|_| panic!("failed to run `{before_dev}`"));
|
||||
@@ -266,13 +278,16 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
|
||||
if !options.no_dev_server && dev_url.is_none() {
|
||||
if let Some(FrontendDist::Directory(path)) = &frontend_dist {
|
||||
if path.exists() {
|
||||
let path = path.canonicalize()?;
|
||||
let path = path
|
||||
.canonicalize()
|
||||
.fs_context("failed to canonicalize path", path.to_path_buf())?;
|
||||
|
||||
let ip = options
|
||||
.host
|
||||
.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1).into());
|
||||
|
||||
let server_url = builtin_dev_server::start(path, ip, options.port)?;
|
||||
let server_url = builtin_dev_server::start(path, ip, options.port)
|
||||
.context("failed to start builtin dev server")?;
|
||||
let server_url = format!("http://{server_url}");
|
||||
dev_url = Some(server_url.parse().unwrap());
|
||||
|
||||
@@ -300,7 +315,7 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
|
||||
let addrs = match host {
|
||||
url::Host::Domain(domain) => {
|
||||
use std::net::ToSocketAddrs;
|
||||
addrs = (domain, port).to_socket_addrs()?;
|
||||
addrs = (domain, port).to_socket_addrs().unwrap();
|
||||
addrs.as_slice()
|
||||
}
|
||||
url::Host::Ipv4(ip) => {
|
||||
@@ -336,6 +351,19 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
|
||||
}
|
||||
}
|
||||
|
||||
if options.additional_watch_folders.is_empty() {
|
||||
options.additional_watch_folders.extend(
|
||||
config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.build
|
||||
.additional_watch_folders
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ use std::{
|
||||
use tauri_utils::mime_type::MimeType;
|
||||
use tokio::sync::broadcast::{channel, Sender};
|
||||
|
||||
use crate::error::ErrorExt;
|
||||
|
||||
const RELOAD_SCRIPT: &str = include_str!("./auto-reload.js");
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -29,7 +31,8 @@ struct ServerState {
|
||||
|
||||
pub fn start<P: AsRef<Path>>(dir: P, ip: IpAddr, port: Option<u16>) -> crate::Result<SocketAddr> {
|
||||
let dir = dir.as_ref();
|
||||
let dir = dunce::canonicalize(dir)?;
|
||||
let dir =
|
||||
dunce::canonicalize(dir).fs_context("failed to canonicalize path", dir.to_path_buf())?;
|
||||
|
||||
// bind port and tcp listener
|
||||
let auto_port = port.is_none();
|
||||
@@ -37,12 +40,12 @@ pub fn start<P: AsRef<Path>>(dir: P, ip: IpAddr, port: Option<u16>) -> crate::Re
|
||||
let (tcp_listener, address) = loop {
|
||||
let address = SocketAddr::new(ip, port);
|
||||
if let Ok(tcp) = std::net::TcpListener::bind(address) {
|
||||
tcp.set_nonblocking(true)?;
|
||||
tcp.set_nonblocking(true).unwrap();
|
||||
break (tcp, address);
|
||||
}
|
||||
|
||||
if !auto_port {
|
||||
anyhow::bail!("Couldn't bind to {port} on {ip}");
|
||||
crate::error::bail!("Couldn't bind to {port} on {ip}");
|
||||
}
|
||||
|
||||
port += 1;
|
||||
@@ -152,11 +155,11 @@ fn inject_address(html_bytes: Vec<u8>, address: &SocketAddr) -> Vec<u8> {
|
||||
}
|
||||
|
||||
fn fs_read_scoped(path: PathBuf, scope: &Path) -> crate::Result<Vec<u8>> {
|
||||
let path = dunce::canonicalize(path)?;
|
||||
let path = dunce::canonicalize(&path).fs_context("failed to canonicalize path", path)?;
|
||||
if path.starts_with(scope) {
|
||||
std::fs::read(path).map_err(Into::into)
|
||||
std::fs::read(&path).fs_context("failed to read file", &path)
|
||||
} else {
|
||||
anyhow::bail!("forbidden path")
|
||||
crate::error::bail!("forbidden path")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
105
crates/tauri-cli/src/error.rs
Normal file
105
crates/tauri-cli/src/error.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{fmt::Display, path::PathBuf};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}: {1}")]
|
||||
Context(String, Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
#[error("{0}")]
|
||||
GenericError(String),
|
||||
#[error("failed to bundle project {0}")]
|
||||
Bundler(#[from] Box<tauri_bundler::Error>),
|
||||
#[error("{context} {path}: {error}")]
|
||||
Fs {
|
||||
context: &'static str,
|
||||
path: PathBuf,
|
||||
error: std::io::Error,
|
||||
},
|
||||
#[error("failed to run command {command}: {error}")]
|
||||
CommandFailed {
|
||||
command: String,
|
||||
error: std::io::Error,
|
||||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error(transparent)]
|
||||
MacosSign(#[from] Box<tauri_macos_sign::Error>),
|
||||
}
|
||||
|
||||
/// Convenient type alias of Result type.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
pub trait Context<T> {
|
||||
// Required methods
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static;
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C;
|
||||
}
|
||||
|
||||
impl<T, E: std::error::Error + Send + Sync + 'static> Context<T> for std::result::Result<T, E> {
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.map_err(|e| Error::Context(context.to_string(), Box::new(e)))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.map_err(|e| Error::Context(f().to_string(), Box::new(e)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Context<T> for Option<T> {
|
||||
fn context<C>(self, context: C) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
{
|
||||
self.ok_or_else(|| Error::GenericError(context.to_string()))
|
||||
}
|
||||
|
||||
fn with_context<C, F>(self, f: F) -> Result<T>
|
||||
where
|
||||
C: Display + Send + Sync + 'static,
|
||||
F: FnOnce() -> C,
|
||||
{
|
||||
self.ok_or_else(|| Error::GenericError(f().to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ErrorExt<T> {
|
||||
fn fs_context(self, context: &'static str, path: impl Into<PathBuf>) -> Result<T>;
|
||||
}
|
||||
|
||||
impl<T> ErrorExt<T> for std::result::Result<T, std::io::Error> {
|
||||
fn fs_context(self, context: &'static str, path: impl Into<PathBuf>) -> Result<T> {
|
||||
self.map_err(|error| Error::Fs {
|
||||
context,
|
||||
path: path.into(),
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! bail {
|
||||
($msg:literal $(,)?) => {
|
||||
return Err(crate::Error::GenericError($msg.into()))
|
||||
};
|
||||
($err:expr $(,)?) => {
|
||||
return Err(crate::Error::GenericError($err))
|
||||
};
|
||||
($fmt:expr, $($arg:tt)*) => {
|
||||
return Err(crate::Error::GenericError(format!($fmt, $($arg)*)))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use bail;
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Context;
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct CargoInstallOptions<'a> {
|
||||
@@ -41,7 +41,7 @@ pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> {
|
||||
cargo.args(["--branch", branch]);
|
||||
}
|
||||
(None, None, None) => {}
|
||||
_ => anyhow::bail!("Only one of --tag, --rev and --branch can be specified"),
|
||||
_ => crate::error::bail!("Only one of --tag, --rev and --branch can be specified"),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -54,9 +54,12 @@ pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
log::info!("Installing Cargo dependency \"{}\"...", options.name);
|
||||
let status = cargo.status().context("failed to run `cargo add`")?;
|
||||
let status = cargo.status().map_err(|error| Error::CommandFailed {
|
||||
command: "cargo add".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("Failed to install Cargo dependency");
|
||||
crate::error::bail!("Failed to install Cargo dependency");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -84,9 +87,12 @@ pub fn uninstall_one(options: CargoUninstallOptions) -> crate::Result<()> {
|
||||
}
|
||||
|
||||
log::info!("Uninstalling Cargo dependency \"{}\"...", options.name);
|
||||
let status = cargo.status().context("failed to run `cargo remove`")?;
|
||||
let status = cargo.status().map_err(|error| Error::CommandFailed {
|
||||
command: "cargo remove".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if !status.success() {
|
||||
anyhow::bail!("Failed to remove Cargo dependency");
|
||||
crate::error::bail!("Failed to remove Cargo dependency");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -10,6 +10,8 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::interface::rust::get_workspace_dir;
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct CargoLockPackage {
|
||||
pub name: String,
|
||||
@@ -49,6 +51,18 @@ pub struct CargoManifest {
|
||||
pub dependencies: HashMap<String, CargoManifestDependency>,
|
||||
}
|
||||
|
||||
pub fn cargo_manifest_and_lock(tauri_dir: &Path) -> (Option<CargoManifest>, Option<CargoLock>) {
|
||||
let manifest: Option<CargoManifest> = fs::read_to_string(tauri_dir.join("Cargo.toml"))
|
||||
.ok()
|
||||
.and_then(|manifest_contents| toml::from_str(&manifest_contents).ok());
|
||||
|
||||
let lock: Option<CargoLock> = get_workspace_dir()
|
||||
.ok()
|
||||
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
|
||||
.and_then(|s| toml::from_str(&s).ok());
|
||||
|
||||
(manifest, lock)
|
||||
}
|
||||
#[derive(Default)]
|
||||
pub struct CrateVersion {
|
||||
pub version: Option<String>,
|
||||
@@ -117,7 +131,7 @@ struct CrateIoGetResponse {
|
||||
pub fn crate_latest_version(name: &str) -> Option<String> {
|
||||
// Reference: https://github.com/rust-lang/crates.io/blob/98c83c8231cbcd15d6b8f06d80a00ad462f71585/src/controllers/krate/metadata.rs#L88
|
||||
let url = format!("https://crates.io/api/v1/crates/{name}?include");
|
||||
let mut response = ureq::get(&url).call().ok()?;
|
||||
let mut response = super::http::get(&url).ok()?;
|
||||
let metadata: CrateIoGetResponse =
|
||||
serde_json::from_reader(response.body_mut().as_reader()).unwrap();
|
||||
metadata.krate.default_version
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// Copyright 2019-2025 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@@ -11,12 +11,14 @@ pub use tauri_utils::{config::*, platform::Target};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
env::{current_dir, set_current_dir, set_var, var_os},
|
||||
env::{current_dir, set_current_dir, set_var},
|
||||
ffi::OsStr,
|
||||
process::exit,
|
||||
sync::{Arc, Mutex, OnceLock},
|
||||
};
|
||||
|
||||
use crate::error::Context;
|
||||
|
||||
pub const MERGE_CONFIG_EXTENSION_NAME: &str = "--config";
|
||||
|
||||
pub struct ConfigMetadata {
|
||||
@@ -70,6 +72,10 @@ pub fn wix_settings(config: WixConfig) -> tauri_bundler::WixSettings {
|
||||
tauri_bundler::WixSettings {
|
||||
version: config.version,
|
||||
upgrade_code: config.upgrade_code,
|
||||
fips_compliant: std::env::var("TAURI_BUNDLER_WIX_FIPS_COMPLIANT")
|
||||
.ok()
|
||||
.map(|v| v == "true")
|
||||
.unwrap_or(config.fips_compliant),
|
||||
language: tauri_bundler::WixLanguage(match config.language {
|
||||
WixLanguage::One(lang) => vec![(lang, Default::default())],
|
||||
WixLanguage::List(languages) => languages
|
||||
@@ -98,7 +104,6 @@ pub fn wix_settings(config: WixConfig) -> tauri_bundler::WixSettings {
|
||||
enable_elevated_update_task: config.enable_elevated_update_task,
|
||||
banner_path: config.banner_path,
|
||||
dialog_image_path: config.dialog_image_path,
|
||||
fips_compliant: var_os("TAURI_BUNDLER_WIX_FIPS_COMPLIANT").is_some_and(|v| v == "true"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +158,8 @@ fn get_internal(
|
||||
|
||||
let tauri_dir = super::app_paths::tauri_dir();
|
||||
let (mut config, config_path) =
|
||||
tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))?;
|
||||
tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))
|
||||
.context("failed to parse config")?;
|
||||
let config_file_name = config_path.file_name().unwrap().to_string_lossy();
|
||||
let mut extensions = HashMap::new();
|
||||
|
||||
@@ -164,7 +170,8 @@ fn get_internal(
|
||||
.map(ToString::to_string);
|
||||
|
||||
if let Some((platform_config, config_path)) =
|
||||
tauri_utils::config::parse::read_platform(target, tauri_dir)?
|
||||
tauri_utils::config::parse::read_platform(target, tauri_dir)
|
||||
.context("failed to parse platform config")?
|
||||
{
|
||||
merge(&mut config, &platform_config);
|
||||
extensions.insert(
|
||||
@@ -188,7 +195,8 @@ fn get_internal(
|
||||
if config_path.extension() == Some(OsStr::new("json"))
|
||||
|| config_path.extension() == Some(OsStr::new("json5"))
|
||||
{
|
||||
let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))?;
|
||||
let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))
|
||||
.context("failed to parse config schema")?;
|
||||
let validator = jsonschema::validator_for(&schema).expect("Invalid schema");
|
||||
let mut errors = validator.iter_errors(&config).peekable();
|
||||
if errors.peek().is_some() {
|
||||
@@ -208,11 +216,11 @@ fn get_internal(
|
||||
|
||||
// the `Config` deserializer for `package > version` can resolve the version from a path relative to the config path
|
||||
// so we actually need to change the current working directory here
|
||||
let current_dir = current_dir()?;
|
||||
set_current_dir(config_path.parent().unwrap())?;
|
||||
let config: Config = serde_json::from_value(config)?;
|
||||
let current_dir = current_dir().context("failed to resolve current directory")?;
|
||||
set_current_dir(config_path.parent().unwrap()).context("failed to set current directory")?;
|
||||
let config: Config = serde_json::from_value(config).context("failed to parse config")?;
|
||||
// revert to previous working directory
|
||||
set_current_dir(current_dir)?;
|
||||
set_current_dir(current_dir).context("failed to set current directory")?;
|
||||
|
||||
for (plugin, conf) in &config.plugins.0 {
|
||||
set_var(
|
||||
@@ -220,7 +228,7 @@ fn get_internal(
|
||||
"TAURI_{}_PLUGIN_CONFIG",
|
||||
plugin.to_uppercase().replace('-', "_")
|
||||
),
|
||||
serde_json::to_string(&conf)?,
|
||||
serde_json::to_string(&conf).context("failed to serialize config")?,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -251,7 +259,7 @@ pub fn reload(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandl
|
||||
if let Some(target) = target {
|
||||
get_internal(merge_configs, true, target)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("config not loaded"))
|
||||
crate::error::bail!("config not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,13 +280,14 @@ pub fn merge_with(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigH
|
||||
let merge_config_str = serde_json::to_string(&merge_config).unwrap();
|
||||
set_var("TAURI_CONFIG", merge_config_str);
|
||||
|
||||
let mut value = serde_json::to_value(config_metadata.inner.clone())?;
|
||||
let mut value =
|
||||
serde_json::to_value(config_metadata.inner.clone()).context("failed to serialize config")?;
|
||||
merge(&mut value, &merge_config);
|
||||
config_metadata.inner = serde_json::from_value(value)?;
|
||||
config_metadata.inner = serde_json::from_value(value).context("failed to parse config")?;
|
||||
|
||||
Ok(handle.clone())
|
||||
} else {
|
||||
Err(anyhow::anyhow!("config not loaded"))
|
||||
crate::error::bail!("config not loaded");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ use std::io;
|
||||
use std::io::{Read, Seek, SeekFrom, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::Context as _;
|
||||
use crate::{error::ErrorExt, Error, Result};
|
||||
use sys::*;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -129,17 +128,25 @@ fn open(path: &Path, opts: &OpenOptions, state: State, msg: &str) -> Result<File
|
||||
// If we want an exclusive lock then if we fail because of NotFound it's
|
||||
// likely because an intermediate directory didn't exist, so try to
|
||||
// create the directory and then continue.
|
||||
let f = opts
|
||||
.open(path)
|
||||
.or_else(|e| {
|
||||
if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
|
||||
create_dir_all(path.parent().unwrap())?;
|
||||
Ok(opts.open(path)?)
|
||||
} else {
|
||||
Err(anyhow::Error::from(e))
|
||||
}
|
||||
})
|
||||
.with_context(|| format!("failed to open: {}", path.display()))?;
|
||||
let f = opts.open(path).or_else(|e| {
|
||||
if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
|
||||
create_dir_all(path.parent().unwrap()).fs_context(
|
||||
"failed to create directory",
|
||||
path.parent().unwrap().to_path_buf(),
|
||||
)?;
|
||||
Ok(
|
||||
opts
|
||||
.open(path)
|
||||
.fs_context("failed to open file", path.to_path_buf())?,
|
||||
)
|
||||
} else {
|
||||
Err(Error::Fs {
|
||||
context: "failed to open file",
|
||||
path: path.to_path_buf(),
|
||||
error: e,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
match state {
|
||||
State::Exclusive => {
|
||||
acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
|
||||
@@ -203,16 +210,18 @@ fn acquire(
|
||||
|
||||
Err(e) => {
|
||||
if !error_contended(&e) {
|
||||
let e = anyhow::Error::from(e);
|
||||
let cx = format!("failed to lock file: {}", path.display());
|
||||
return Err(e.context(cx));
|
||||
return Err(Error::Fs {
|
||||
context: "failed to lock file",
|
||||
path: path.to_path_buf(),
|
||||
error: e,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let msg = format!("waiting for file lock on {msg}");
|
||||
log::info!(action = "Blocking"; "{}", &msg);
|
||||
|
||||
lock_block().with_context(|| format!("failed to lock file: {}", path.display()))?;
|
||||
lock_block().fs_context("failed to lock file", path.to_path_buf())?;
|
||||
return Ok(());
|
||||
|
||||
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
|
||||
|
||||
@@ -2,20 +2,54 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Result;
|
||||
use std::path::Path;
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
Error,
|
||||
};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> Result<()> {
|
||||
pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
|
||||
let from = from.as_ref();
|
||||
let to = to.as_ref();
|
||||
if !from.exists() {
|
||||
return Err(anyhow::anyhow!("{:?} does not exist", from));
|
||||
Err(Error::Fs {
|
||||
context: "failed to copy file",
|
||||
path: from.to_path_buf(),
|
||||
error: std::io::Error::new(std::io::ErrorKind::NotFound, "source does not exist"),
|
||||
})?;
|
||||
}
|
||||
if !from.is_file() {
|
||||
return Err(anyhow::anyhow!("{:?} is not a file", from));
|
||||
Err(Error::Fs {
|
||||
context: "failed to copy file",
|
||||
path: from.to_path_buf(),
|
||||
error: std::io::Error::other("not a file"),
|
||||
})?;
|
||||
}
|
||||
let dest_dir = to.parent().expect("No data in parent");
|
||||
std::fs::create_dir_all(dest_dir)?;
|
||||
std::fs::copy(from, to)?;
|
||||
std::fs::create_dir_all(dest_dir)
|
||||
.fs_context("failed to create directory", dest_dir.to_path_buf())?;
|
||||
std::fs::copy(from, to).fs_context("failed to copy file", from.to_path_buf())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Find an entry in a directory matching a glob pattern.
|
||||
/// Currently does not traverse subdirectories.
|
||||
// currently only used on macOS
|
||||
#[allow(dead_code)]
|
||||
pub fn find_in_directory(path: &Path, glob_pattern: &str) -> crate::Result<PathBuf> {
|
||||
let pattern = glob::Pattern::new(glob_pattern)
|
||||
.with_context(|| format!("failed to parse glob pattern {glob_pattern}"))?;
|
||||
for entry in std::fs::read_dir(path)
|
||||
.with_context(|| format!("failed to read directory {}", path.display()))?
|
||||
{
|
||||
let entry = entry.context("failed to read directory entry")?;
|
||||
if pattern.matches_path(&entry.path()) {
|
||||
return Ok(entry.path());
|
||||
}
|
||||
}
|
||||
crate::error::bail!(
|
||||
"No file found in {} matching {}",
|
||||
path.display(),
|
||||
glob_pattern
|
||||
)
|
||||
}
|
||||
|
||||
26
crates/tauri-cli/src/helpers/http.rs
Normal file
26
crates/tauri-cli/src/helpers/http.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use ureq::{http::Response, Agent, Body};
|
||||
|
||||
const CLI_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
|
||||
|
||||
pub fn get(url: &str) -> Result<Response<Body>, ureq::Error> {
|
||||
#[allow(unused_mut)]
|
||||
let mut config_builder = ureq::Agent::config_builder()
|
||||
.user_agent(CLI_USER_AGENT)
|
||||
.proxy(ureq::Proxy::try_from_env());
|
||||
|
||||
#[cfg(feature = "platform-certs")]
|
||||
{
|
||||
config_builder = config_builder.tls_config(
|
||||
ureq::tls::TlsConfig::builder()
|
||||
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
|
||||
.build(),
|
||||
);
|
||||
}
|
||||
|
||||
let agent: Agent = config_builder.build().into();
|
||||
agent.get(url).call()
|
||||
}
|
||||
@@ -9,9 +9,12 @@ pub mod config;
|
||||
pub mod flock;
|
||||
pub mod framework;
|
||||
pub mod fs;
|
||||
pub mod http;
|
||||
pub mod npm;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod pbxproj;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod plist;
|
||||
pub mod plugins;
|
||||
pub mod prompts;
|
||||
pub mod template;
|
||||
@@ -23,9 +26,10 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use tauri_utils::config::HookCommand;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
use crate::Error;
|
||||
use crate::{
|
||||
interface::{AppInterface, Interface},
|
||||
CommandExt,
|
||||
@@ -97,7 +101,10 @@ pub fn run_hook(
|
||||
.current_dir(cwd)
|
||||
.envs(env)
|
||||
.piped()
|
||||
.with_context(|| format!("failed to run `{script}` with `cmd /C`"))?;
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: script.clone(),
|
||||
error,
|
||||
})?;
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let status = Command::new("sh")
|
||||
.arg("-c")
|
||||
@@ -105,10 +112,13 @@ pub fn run_hook(
|
||||
.current_dir(cwd)
|
||||
.envs(env)
|
||||
.piped()
|
||||
.with_context(|| format!("failed to run `{script}` with `sh -c`"))?;
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: script.clone(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!(
|
||||
crate::error::bail!(
|
||||
"{} `{}` failed with exit code {}",
|
||||
name,
|
||||
script,
|
||||
@@ -122,6 +132,7 @@ pub fn run_hook(
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn strip_semver_prerelease_tag(version: &mut semver::Version) -> crate::Result<()> {
|
||||
use crate::error::Context;
|
||||
if !version.pre.is_empty() {
|
||||
if let Some((_prerelease_tag, number)) = version.pre.as_str().to_string().split_once('.') {
|
||||
version.pre = semver::Prerelease::EMPTY;
|
||||
@@ -133,7 +144,11 @@ pub fn strip_semver_prerelease_tag(version: &mut semver::Version) -> crate::Resu
|
||||
format!(".{}", version.build.as_str())
|
||||
}
|
||||
))
|
||||
.with_context(|| format!("bundle version {number:?} prerelease is invalid"))?;
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to parse {version} as semver: bundle version {number:?} prerelease is invalid"
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::helpers::cross_command;
|
||||
use std::{fmt::Display, path::Path, process::Command};
|
||||
use crate::{
|
||||
error::{Context, Error},
|
||||
helpers::cross_command,
|
||||
};
|
||||
use std::{collections::HashMap, fmt::Display, path::Path, process::Command};
|
||||
|
||||
pub fn manager_version(package_manager: &str) -> Option<String> {
|
||||
cross_command(package_manager)
|
||||
@@ -150,10 +153,13 @@ impl PackageManager {
|
||||
let status = command
|
||||
.current_dir(frontend_dir)
|
||||
.status()
|
||||
.with_context(|| format!("failed to run {self}"))?;
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: format!("failed to run {self}"),
|
||||
error,
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("Failed to install NPM {dependencies_str}");
|
||||
crate::error::bail!("Failed to install NPM {dependencies_str}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -188,15 +194,19 @@ impl PackageManager {
|
||||
.args(dependencies)
|
||||
.current_dir(frontend_dir)
|
||||
.status()
|
||||
.with_context(|| format!("failed to run {self}"))?;
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: format!("failed to run {self}"),
|
||||
error,
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
anyhow::bail!("Failed to remove NPM {dependencies_str}");
|
||||
crate::error::bail!("Failed to remove NPM {dependencies_str}");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Use `current_package_versions` as much as possible for better speed
|
||||
pub fn current_package_version<P: AsRef<Path>>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -209,7 +219,11 @@ impl PackageManager {
|
||||
.arg(name)
|
||||
.args(["--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn list --pattern".to_string(),
|
||||
error,
|
||||
})?,
|
||||
None,
|
||||
),
|
||||
PackageManager::YarnBerry => (
|
||||
@@ -218,7 +232,11 @@ impl PackageManager {
|
||||
.arg(name)
|
||||
.arg("--json")
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn info --json".to_string(),
|
||||
error,
|
||||
})?,
|
||||
Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
|
||||
),
|
||||
PackageManager::Pnpm => (
|
||||
@@ -227,7 +245,11 @@ impl PackageManager {
|
||||
.arg(name)
|
||||
.args(["--parseable", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "pnpm list --parseable --depth 0".to_string(),
|
||||
error,
|
||||
})?,
|
||||
None,
|
||||
),
|
||||
// Bun and Deno don't support `list` command
|
||||
@@ -237,7 +259,11 @@ impl PackageManager {
|
||||
.arg(name)
|
||||
.args(["version", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()?,
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "npm list --version --depth 0".to_string(),
|
||||
error,
|
||||
})?,
|
||||
None,
|
||||
),
|
||||
};
|
||||
@@ -254,4 +280,180 @@ impl PackageManager {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_package_versions(
|
||||
&self,
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = match self {
|
||||
PackageManager::Yarn => return yarn_package_versions(packages, frontend_dir),
|
||||
PackageManager::YarnBerry => return yarn_berry_package_versions(packages, frontend_dir),
|
||||
PackageManager::Pnpm => cross_command("pnpm")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "pnpm list --json --depth 0".to_string(),
|
||||
error,
|
||||
})?,
|
||||
// Bun and Deno don't support `list` command
|
||||
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "npm list --json --depth 0".to_string(),
|
||||
error,
|
||||
})?,
|
||||
};
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct ListOutput {
|
||||
#[serde(default)]
|
||||
dependencies: HashMap<String, ListDependency>,
|
||||
#[serde(default)]
|
||||
dev_dependencies: HashMap<String, ListDependency>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ListDependency {
|
||||
version: String,
|
||||
}
|
||||
|
||||
let json = if matches!(self, PackageManager::Pnpm) {
|
||||
serde_json::from_str::<Vec<ListOutput>>(&stdout)
|
||||
.ok()
|
||||
.and_then(|out| out.into_iter().next())
|
||||
.context("failed to parse pnpm list")?
|
||||
} else {
|
||||
serde_json::from_str::<ListOutput>(&stdout).context("failed to parse npm list")?
|
||||
};
|
||||
for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) {
|
||||
let version = dependency.version;
|
||||
if let Ok(version) = semver::Version::parse(&version) {
|
||||
versions.insert(package, version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{package}`");
|
||||
}
|
||||
}
|
||||
Ok(versions)
|
||||
}
|
||||
}
|
||||
|
||||
fn yarn_package_versions(
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = cross_command("yarn")
|
||||
.arg("list")
|
||||
.args(packages)
|
||||
.args(["--json", "--depth", "0"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn list --json --depth 0".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutput {
|
||||
data: YarnListOutputData,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutputData {
|
||||
trees: Vec<YarnListOutputDataTree>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnListOutputDataTree {
|
||||
name: String,
|
||||
}
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Ok(tree) = serde_json::from_str::<YarnListOutput>(line) {
|
||||
for tree in tree.data.trees {
|
||||
let Some((name, version)) = tree.name.rsplit_once('@') else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(version) = semver::Version::parse(version) {
|
||||
versions.insert(name.to_owned(), version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
|
||||
}
|
||||
}
|
||||
return Ok(versions);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
fn yarn_berry_package_versions(
|
||||
packages: &[String],
|
||||
frontend_dir: &Path,
|
||||
) -> crate::Result<HashMap<String, semver::Version>> {
|
||||
let output = cross_command("yarn")
|
||||
.args(["info", "--json"])
|
||||
.current_dir(frontend_dir)
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn info --json".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let mut versions = HashMap::new();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
if !output.status.success() {
|
||||
return Ok(versions);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnBerryInfoOutput {
|
||||
value: String,
|
||||
children: YarnBerryInfoOutputChildren,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct YarnBerryInfoOutputChildren {
|
||||
version: String,
|
||||
}
|
||||
|
||||
for line in stdout.lines() {
|
||||
if let Ok(info) = serde_json::from_str::<YarnBerryInfoOutput>(line) {
|
||||
let Some((name, _)) = info.value.rsplit_once('@') else {
|
||||
continue;
|
||||
};
|
||||
if !packages.iter().any(|package| package == name) {
|
||||
continue;
|
||||
}
|
||||
let version = info.children.version;
|
||||
if let Ok(version) = semver::Version::parse(&version) {
|
||||
versions.insert(name.to_owned(), version);
|
||||
} else {
|
||||
log::error!("Failed to parse version `{version}` for NPM package `{name}`");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(versions)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,12 @@ use std::{
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::error::ErrorExt;
|
||||
|
||||
pub fn parse<P: AsRef<Path>>(path: P) -> crate::Result<Pbxproj> {
|
||||
let path = path.as_ref();
|
||||
let pbxproj = std::fs::read_to_string(path)?;
|
||||
let pbxproj =
|
||||
std::fs::read_to_string(path).fs_context("failed to read pbxproj file", path.to_path_buf())?;
|
||||
|
||||
let mut proj = Pbxproj {
|
||||
path: path.to_owned(),
|
||||
@@ -171,7 +174,7 @@ enum State {
|
||||
}
|
||||
|
||||
pub struct Pbxproj {
|
||||
path: PathBuf,
|
||||
pub path: PathBuf,
|
||||
raw_lines: Vec<String>,
|
||||
pub xc_build_configuration: BTreeMap<String, XCBuildConfiguration>,
|
||||
pub xc_configuration_list: BTreeMap<String, XCConfigurationList>,
|
||||
|
||||
42
crates/tauri-cli/src/helpers/plist.rs
Normal file
42
crates/tauri-cli/src/helpers/plist.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::Context;
|
||||
|
||||
pub enum PlistKind {
|
||||
Path(PathBuf),
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
impl From<PathBuf> for PlistKind {
|
||||
fn from(p: PathBuf) -> Self {
|
||||
Self::Path(p)
|
||||
}
|
||||
}
|
||||
impl From<plist::Value> for PlistKind {
|
||||
fn from(p: plist::Value) -> Self {
|
||||
Self::Plist(p)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_plist(src: Vec<PlistKind>) -> crate::Result<plist::Value> {
|
||||
let mut merged_plist = plist::Dictionary::new();
|
||||
|
||||
for plist_kind in src {
|
||||
let src_plist = match plist_kind {
|
||||
PlistKind::Path(p) => plist::Value::from_file(&p)
|
||||
.with_context(|| format!("failed to parse plist from {}", p.display()))?,
|
||||
PlistKind::Plist(v) => v,
|
||||
};
|
||||
if let Some(dict) = src_plist.into_dictionary() {
|
||||
for (key, value) in dict {
|
||||
merged_plist.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plist::Value::Dictionary(merged_plist))
|
||||
}
|
||||
@@ -38,6 +38,7 @@ pub fn known_plugins() -> HashMap<&'static str, PluginMetadata> {
|
||||
|
||||
// uses builder pattern
|
||||
for p in [
|
||||
"autostart",
|
||||
"global-shortcut",
|
||||
"localhost",
|
||||
"log",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use crate::Result;
|
||||
use crate::{error::Context, Result};
|
||||
|
||||
pub fn input<T>(
|
||||
prompt: &str,
|
||||
@@ -32,7 +32,7 @@ where
|
||||
builder
|
||||
.interact_text()
|
||||
.map(|t: T| if t.ne("") { Some(t) } else { None })
|
||||
.map_err(Into::into)
|
||||
.context("failed to prompt input")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ pub fn confirm(prompt: &str, default: Option<bool>) -> Result<bool> {
|
||||
if let Some(default) = default {
|
||||
builder = builder.default(default);
|
||||
}
|
||||
builder.interact().map_err(Into::into)
|
||||
builder.interact().context("failed to prompt confirm")
|
||||
}
|
||||
|
||||
pub fn multiselect<T: ToString>(
|
||||
@@ -57,5 +57,5 @@ pub fn multiselect<T: ToString>(
|
||||
if let Some(defaults) = defaults {
|
||||
builder = builder.defaults(defaults);
|
||||
}
|
||||
builder.interact().map_err(Into::into)
|
||||
builder.interact().context("failed to prompt multi-select")
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ use include_dir::Dir;
|
||||
use serde::Serialize;
|
||||
use serde_json::value::{Map, Value as JsonValue};
|
||||
|
||||
use crate::error::ErrorExt;
|
||||
|
||||
/// Map of template variable names and values.
|
||||
#[derive(Clone, Debug)]
|
||||
#[repr(transparent)]
|
||||
@@ -74,13 +76,17 @@ pub fn render_with_generator<
|
||||
file_path.set_extension("toml");
|
||||
}
|
||||
}
|
||||
if let Some(mut output_file) = out_file_generator(file_path)? {
|
||||
if let Some(mut output_file) = out_file_generator(file_path.clone())
|
||||
.fs_context("failed to generate output file", file_path.clone())?
|
||||
{
|
||||
if let Some(utf8) = file.contents_utf8() {
|
||||
handlebars
|
||||
.render_template_to_write(utf8, &data, &mut output_file)
|
||||
.expect("Failed to render template");
|
||||
} else {
|
||||
output_file.write_all(file.contents())?;
|
||||
output_file
|
||||
.write_all(file.contents())
|
||||
.fs_context("failed to write template", file_path.clone())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Context;
|
||||
use base64::Engine;
|
||||
use minisign::{
|
||||
sign, KeyPair as KP, PublicKey, PublicKeyBox, SecretKey, SecretKeyBox, SignatureBox,
|
||||
@@ -15,6 +14,8 @@ use std::{
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use crate::error::{Context, ErrorExt};
|
||||
|
||||
/// A key pair (`PublicKey` and `SecretKey`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeyPair {
|
||||
@@ -24,9 +25,9 @@ pub struct KeyPair {
|
||||
|
||||
fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
fs::create_dir_all(parent).fs_context("failed to create directory", parent.to_path_buf())?;
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
let file = File::create(path).fs_context("failed to create file", path.to_path_buf())?;
|
||||
Ok(BufWriter::new(file))
|
||||
}
|
||||
|
||||
@@ -48,8 +49,12 @@ pub fn generate_key(password: Option<String>) -> crate::Result<KeyPair> {
|
||||
|
||||
/// Transform a base64 String to readable string for the main signer
|
||||
pub fn decode_key<S: AsRef<[u8]>>(base64_key: S) -> crate::Result<String> {
|
||||
let decoded_str = &base64::engine::general_purpose::STANDARD.decode(base64_key)?[..];
|
||||
Ok(String::from(str::from_utf8(decoded_str)?))
|
||||
let decoded_str = &base64::engine::general_purpose::STANDARD
|
||||
.decode(base64_key)
|
||||
.context("failed to decode base64 key")?[..];
|
||||
Ok(String::from(
|
||||
str::from_utf8(decoded_str).context("failed to convert base64 to utf8")?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Save KeyPair to disk
|
||||
@@ -69,28 +74,43 @@ where
|
||||
|
||||
if sk_path.exists() {
|
||||
if !force {
|
||||
return Err(anyhow::anyhow!(
|
||||
crate::error::bail!(
|
||||
"Key generation aborted:\n{} already exists\nIf you really want to overwrite the existing key pair, add the --force switch to force this operation.",
|
||||
sk_path.display()
|
||||
));
|
||||
);
|
||||
} else {
|
||||
std::fs::remove_file(sk_path)?;
|
||||
std::fs::remove_file(sk_path)
|
||||
.fs_context("failed to remove secret key file", sk_path.to_path_buf())?;
|
||||
}
|
||||
}
|
||||
|
||||
if pk_path.exists() {
|
||||
std::fs::remove_file(pk_path)?;
|
||||
std::fs::remove_file(pk_path)
|
||||
.fs_context("failed to remove public key file", pk_path.to_path_buf())?;
|
||||
}
|
||||
|
||||
let mut sk_writer = create_file(sk_path)?;
|
||||
write!(sk_writer, "{key:}")?;
|
||||
sk_writer.flush()?;
|
||||
let write_file = |mut writer: BufWriter<File>, contents: &str| -> std::io::Result<()> {
|
||||
write!(writer, "{contents:}")?;
|
||||
writer.flush()?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
let mut pk_writer = create_file(pk_path)?;
|
||||
write!(pk_writer, "{pubkey:}")?;
|
||||
pk_writer.flush()?;
|
||||
write_file(create_file(sk_path)?, key)
|
||||
.fs_context("failed to write secret key", sk_path.to_path_buf())?;
|
||||
|
||||
Ok((fs::canonicalize(sk_path)?, fs::canonicalize(pk_path)?))
|
||||
write_file(create_file(pk_path)?, pubkey)
|
||||
.fs_context("failed to write public key", pk_path.to_path_buf())?;
|
||||
|
||||
Ok((
|
||||
fs::canonicalize(sk_path).fs_context(
|
||||
"failed to canonicalize secret key path",
|
||||
sk_path.to_path_buf(),
|
||||
)?,
|
||||
fs::canonicalize(pk_path).fs_context(
|
||||
"failed to canonicalize public key path",
|
||||
pk_path.to_path_buf(),
|
||||
)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Sign files
|
||||
@@ -104,8 +124,6 @@ where
|
||||
extension.push(".sig");
|
||||
let signature_path = bin_path.with_extension(extension);
|
||||
|
||||
let mut signature_box_writer = create_file(&signature_path)?;
|
||||
|
||||
let trusted_comment = format!(
|
||||
"timestamp:{}\tfile:{}",
|
||||
unix_timestamp(),
|
||||
@@ -120,13 +138,18 @@ where
|
||||
data_reader,
|
||||
Some(trusted_comment.as_str()),
|
||||
Some("signature from tauri secret key"),
|
||||
)?;
|
||||
)
|
||||
.context("failed to sign file")?;
|
||||
|
||||
let encoded_signature =
|
||||
base64::engine::general_purpose::STANDARD.encode(signature_box.to_string());
|
||||
signature_box_writer.write_all(encoded_signature.as_bytes())?;
|
||||
signature_box_writer.flush()?;
|
||||
Ok((fs::canonicalize(&signature_path)?, signature_box))
|
||||
std::fs::write(&signature_path, encoded_signature.as_bytes())
|
||||
.fs_context("failed to write signature file", signature_path.clone())?;
|
||||
Ok((
|
||||
fs::canonicalize(&signature_path)
|
||||
.fs_context("failed to canonicalize signature file", &signature_path)?,
|
||||
signature_box,
|
||||
))
|
||||
}
|
||||
|
||||
/// Gets the updater secret key from the given private key and password.
|
||||
@@ -148,7 +171,9 @@ pub fn pub_key<S: AsRef<[u8]>>(public_key: S) -> crate::Result<PublicKey> {
|
||||
let decoded_publick = decode_key(public_key).context("failed to decode base64 pubkey")?;
|
||||
let pk_box =
|
||||
PublicKeyBox::from_string(&decoded_publick).context("failed to load updater pubkey")?;
|
||||
let pk = pk_box.into_public_key()?;
|
||||
let pk = pk_box
|
||||
.into_public_key()
|
||||
.context("failed to convert updater pubkey")?;
|
||||
Ok(pk)
|
||||
}
|
||||
|
||||
@@ -168,7 +193,7 @@ where
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.open(data_path)
|
||||
.map_err(|e| minisign::PError::new(minisign::ErrorKind::Io, e))?;
|
||||
.fs_context("failed to open data file", data_path.to_path_buf())?;
|
||||
Ok(BufReader::new(file))
|
||||
}
|
||||
|
||||
@@ -176,7 +201,7 @@ where
|
||||
mod tests {
|
||||
const PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5dkpDN09RZm5GeVAzc2RuYlNzWVVJelJRQnNIV2JUcGVXZUplWXZXYXpqUUFBQkFBQUFBQUFBQUFBQUlBQUFBQTZrN2RnWGh5dURxSzZiL1ZQSDdNcktiaHRxczQwMXdQelRHbjRNcGVlY1BLMTBxR2dpa3I3dDE1UTVDRDE4MXR4WlQwa1BQaXdxKy9UU2J2QmVSNXhOQWFDeG1GSVllbUNpTGJQRkhhTnROR3I5RmdUZi90OGtvaGhJS1ZTcjdZU0NyYzhQWlQ5cGM9Cg==";
|
||||
|
||||
// we use minisign=0.7.3 to prevent a breaking change
|
||||
// minisign >=0.7.4,<0.8.0 couldn't handle empty passwords.
|
||||
#[test]
|
||||
fn empty_password_is_valid() {
|
||||
let path = std::env::temp_dir().join("minisign-password-text.txt");
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{helpers::app_paths::tauri_dir, Result};
|
||||
use crate::{
|
||||
error::{Context, Error, ErrorExt},
|
||||
helpers::app_paths::tauri_dir,
|
||||
Result,
|
||||
};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
fs::{create_dir_all, File},
|
||||
io::{BufWriter, Write},
|
||||
@@ -13,7 +18,6 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use icns::{IconFamily, IconType};
|
||||
use image::{
|
||||
@@ -22,8 +26,9 @@ use image::{
|
||||
png::{CompressionType, FilterType as PngFilterType, PngEncoder},
|
||||
},
|
||||
imageops::FilterType,
|
||||
open, DynamicImage, ExtendedColorType, ImageBuffer, ImageEncoder, Rgba,
|
||||
open, DynamicImage, ExtendedColorType, GenericImageView, ImageBuffer, ImageEncoder, Pixel, Rgba,
|
||||
};
|
||||
use rayon::iter::ParallelIterator;
|
||||
use resvg::{tiny_skia, usvg};
|
||||
use serde::Deserialize;
|
||||
|
||||
@@ -40,10 +45,48 @@ struct PngEntry {
|
||||
out_path: PathBuf,
|
||||
}
|
||||
|
||||
enum AndroidIconKind {
|
||||
Regular,
|
||||
Rounded,
|
||||
}
|
||||
|
||||
struct AndroidEntries {
|
||||
icon: Vec<(PngEntry, AndroidIconKind)>,
|
||||
foreground: Vec<PngEntry>,
|
||||
background: Vec<PngEntry>,
|
||||
monochrome: Vec<PngEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Manifest {
|
||||
default: String,
|
||||
bg_color: Option<String>,
|
||||
android_bg: Option<String>,
|
||||
android_fg: Option<String>,
|
||||
android_monochrome: Option<String>,
|
||||
android_fg_scale: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(about = "Generate various icons for all major platforms")]
|
||||
pub struct Options {
|
||||
/// Path to the source icon (squared PNG or SVG file with transparency).
|
||||
/// Path to the source icon (squared PNG or SVG file with transparency) or a manifest file.
|
||||
///
|
||||
/// The manifest file is a JSON file with the following structure:
|
||||
/// {
|
||||
/// "default": "app-icon.png",
|
||||
/// "bg_color": "#fff",
|
||||
/// "android_bg": "app-icon-bg.png",
|
||||
/// "android_fg": "app-icon-fg.png",
|
||||
/// "android_fg_scale": 85,
|
||||
/// "android_monochrome": "app-icon-monochrome.png"
|
||||
/// }
|
||||
///
|
||||
/// All file paths defined in the manifest JSON are relative to the manifest file path.
|
||||
///
|
||||
/// Only the `default` manifest property is required.
|
||||
///
|
||||
/// The `bg_color` manifest value overwrites the `--ios-color` option if set.
|
||||
#[clap(default_value = "./app-icon.png")]
|
||||
input: PathBuf,
|
||||
/// Output directory.
|
||||
@@ -60,6 +103,7 @@ pub struct Options {
|
||||
ios_color: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum Source {
|
||||
Svg(resvg::usvg::Tree),
|
||||
@@ -81,7 +125,7 @@ impl Source {
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_exact(&self, size: u32) -> Result<DynamicImage> {
|
||||
fn resize_exact(&self, size: u32) -> DynamicImage {
|
||||
match self {
|
||||
Self::Svg(svg) => {
|
||||
let mut pixmap = tiny_skia::Pixmap::new(size, size).unwrap();
|
||||
@@ -91,14 +135,105 @@ impl Source {
|
||||
tiny_skia::Transform::from_scale(scale, scale),
|
||||
&mut pixmap.as_mut(),
|
||||
);
|
||||
let img_buffer = ImageBuffer::from_raw(size, size, pixmap.take()).unwrap();
|
||||
Ok(DynamicImage::ImageRgba8(img_buffer))
|
||||
// Switch to use `Pixmap::take_demultiplied` in the future when it's published
|
||||
// https://github.com/linebender/tiny-skia/blob/624257c0feb394bf6c4d0d688f8ea8030aae320f/src/pixmap.rs#L266
|
||||
let img_buffer = ImageBuffer::from_par_fn(size, size, |x, y| {
|
||||
let pixel = pixmap.pixel(x, y).unwrap().demultiply();
|
||||
Rgba([pixel.red(), pixel.green(), pixel.blue(), pixel.alpha()])
|
||||
});
|
||||
DynamicImage::ImageRgba8(img_buffer)
|
||||
}
|
||||
Self::DynamicImage(image) => {
|
||||
// image.resize_exact(size, size, FilterType::Lanczos3)
|
||||
resize_image(image, size, size)
|
||||
}
|
||||
Self::DynamicImage(i) => Ok(i.resize_exact(size, size, FilterType::Lanczos3)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `image` does not use premultiplied alpha in resize, so we do it manually here,
|
||||
// see https://github.com/image-rs/image/issues/1655
|
||||
fn resize_image(image: &DynamicImage, new_width: u32, new_height: u32) -> DynamicImage {
|
||||
// Premultiply alpha
|
||||
let premultiplied_image = ImageBuffer::from_par_fn(image.width(), image.height(), |x, y| {
|
||||
let mut pixel = image.get_pixel(x, y);
|
||||
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
|
||||
pixel.apply_without_alpha(|channel_value| (channel_value as f32 * alpha) as u8);
|
||||
pixel
|
||||
});
|
||||
|
||||
let mut resized = image::imageops::resize(
|
||||
&premultiplied_image,
|
||||
new_width,
|
||||
new_height,
|
||||
FilterType::Lanczos3,
|
||||
);
|
||||
|
||||
// Demultiply alpha
|
||||
resized.par_pixels_mut().for_each(|pixel| {
|
||||
let alpha = pixel.0[3] as f32 / u8::MAX as f32;
|
||||
pixel.apply_without_alpha(|channel_value| (channel_value as f32 / alpha) as u8);
|
||||
});
|
||||
|
||||
DynamicImage::ImageRgba8(resized)
|
||||
}
|
||||
|
||||
fn read_source(path: PathBuf) -> Result<Source> {
|
||||
if let Some(extension) = path.extension() {
|
||||
if extension == "svg" {
|
||||
let rtree = {
|
||||
let mut fontdb = usvg::fontdb::Database::new();
|
||||
fontdb.load_system_fonts();
|
||||
|
||||
let opt = usvg::Options {
|
||||
// Get file's absolute directory.
|
||||
resources_dir: std::fs::canonicalize(&path)
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf())),
|
||||
fontdb: Arc::new(fontdb),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let svg_data = std::fs::read(&path).fs_context("Failed to read source icon", &path)?;
|
||||
usvg::Tree::from_data(&svg_data, &opt).unwrap()
|
||||
};
|
||||
|
||||
Ok(Source::Svg(rtree))
|
||||
} else {
|
||||
Ok(Source::DynamicImage(DynamicImage::ImageRgba8(
|
||||
open(&path)
|
||||
.context(format!(
|
||||
"failed to read and decode source image {}",
|
||||
path.display()
|
||||
))?
|
||||
.into_rgba8(),
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
crate::error::bail!("Error loading image");
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bg_color(bg_color_string: &String) -> Result<Rgba<u8>> {
|
||||
let bg_color = css_color::Srgb::from_str(bg_color_string)
|
||||
.map(|color| {
|
||||
Rgba([
|
||||
(color.red * 255.) as u8,
|
||||
(color.green * 255.) as u8,
|
||||
(color.blue * 255.) as u8,
|
||||
(color.alpha * 255.) as u8,
|
||||
])
|
||||
})
|
||||
.map_err(|_e| {
|
||||
Error::Context(
|
||||
format!("failed to parse color {bg_color_string}"),
|
||||
"invalid RGBA color".into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(bg_color)
|
||||
}
|
||||
|
||||
pub fn command(options: Options) -> Result<()> {
|
||||
let input = options.input;
|
||||
let out_dir = options.output.unwrap_or_else(|| {
|
||||
@@ -106,52 +241,34 @@ pub fn command(options: Options) -> Result<()> {
|
||||
tauri_dir().join("icons")
|
||||
});
|
||||
let png_icon_sizes = options.png.unwrap_or_default();
|
||||
let ios_color = css_color::Srgb::from_str(&options.ios_color)
|
||||
.map(|color| {
|
||||
Rgba([
|
||||
(color.red * 255.) as u8,
|
||||
(color.green * 255.) as u8,
|
||||
(color.blue * 255.) as u8,
|
||||
(color.alpha * 255.) as u8,
|
||||
])
|
||||
})
|
||||
.map_err(|_| anyhow::anyhow!("failed to parse iOS color"))?;
|
||||
|
||||
create_dir_all(&out_dir).context("Can't create output directory")?;
|
||||
create_dir_all(&out_dir).fs_context("Can't create output directory", &out_dir)?;
|
||||
|
||||
let source = if let Some(extension) = input.extension() {
|
||||
if extension == "svg" {
|
||||
let rtree = {
|
||||
let mut fontdb = usvg::fontdb::Database::new();
|
||||
fontdb.load_system_fonts();
|
||||
|
||||
let opt = usvg::Options {
|
||||
// Get file's absolute directory.
|
||||
resources_dir: std::fs::canonicalize(&input)
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|p| p.to_path_buf())),
|
||||
fontdb: Arc::new(fontdb),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let svg_data = std::fs::read(&input).unwrap();
|
||||
usvg::Tree::from_data(&svg_data, &opt).unwrap()
|
||||
};
|
||||
|
||||
Source::Svg(rtree)
|
||||
} else {
|
||||
Source::DynamicImage(DynamicImage::ImageRgba8(
|
||||
open(&input)
|
||||
.context("Can't read and decode source image")?
|
||||
.into_rgba8(),
|
||||
))
|
||||
}
|
||||
let manifest = if input.extension().is_some_and(|ext| ext == "json") {
|
||||
parse_manifest(&input).map(Some)?
|
||||
} else {
|
||||
anyhow::bail!("Error loading image");
|
||||
None
|
||||
};
|
||||
|
||||
let bg_color_string = match manifest {
|
||||
Some(ref manifest) => manifest
|
||||
.bg_color
|
||||
.as_ref()
|
||||
.unwrap_or(&options.ios_color)
|
||||
.clone(),
|
||||
None => options.ios_color,
|
||||
};
|
||||
let bg_color = parse_bg_color(&bg_color_string)?;
|
||||
|
||||
let default_icon = match manifest {
|
||||
Some(ref manifest) => input.parent().unwrap().join(manifest.default.clone()),
|
||||
None => input.clone(),
|
||||
};
|
||||
|
||||
let source = read_source(default_icon)?;
|
||||
|
||||
if source.height() != source.width() {
|
||||
anyhow::bail!("Source image must be square");
|
||||
crate::error::bail!("Source image must be square");
|
||||
}
|
||||
|
||||
if png_icon_sizes.is_empty() {
|
||||
@@ -159,38 +276,49 @@ pub fn command(options: Options) -> Result<()> {
|
||||
icns(&source, &out_dir).context("Failed to generate .icns file")?;
|
||||
ico(&source, &out_dir).context("Failed to generate .ico file")?;
|
||||
|
||||
png(&source, &out_dir, ios_color).context("Failed to generate png icons")?;
|
||||
png(&source, &out_dir, bg_color).context("Failed to generate png icons")?;
|
||||
android(&source, &input, manifest, &bg_color_string, &out_dir)
|
||||
.context("Failed to generate android icons")?;
|
||||
} else {
|
||||
for target in png_icon_sizes
|
||||
.into_iter()
|
||||
.map(|size| {
|
||||
let name = format!("{size}x{size}.png");
|
||||
let out_path = out_dir.join(&name);
|
||||
PngEntry {
|
||||
name,
|
||||
out_path,
|
||||
size,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<PngEntry>>()
|
||||
{
|
||||
for target in png_icon_sizes.into_iter().map(|size| {
|
||||
let name = format!("{size}x{size}.png");
|
||||
let out_path = out_dir.join(&name);
|
||||
PngEntry {
|
||||
name,
|
||||
out_path,
|
||||
size,
|
||||
}
|
||||
}) {
|
||||
log::info!(action = "PNG"; "Creating {}", target.name);
|
||||
resize_and_save_png(&source, target.size, &target.out_path, None)?;
|
||||
resize_and_save_png(&source, target.size, &target.out_path, None, None)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_manifest(manifest_path: &Path) -> Result<Manifest> {
|
||||
let manifest: Manifest = serde_json::from_str(
|
||||
&std::fs::read_to_string(manifest_path)
|
||||
.fs_context("cannot read manifest file", manifest_path)?,
|
||||
)
|
||||
.context(format!(
|
||||
"failed to parse manifest file {}",
|
||||
manifest_path.display()
|
||||
))?;
|
||||
log::debug!("Read manifest file from {}", manifest_path.display());
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
fn appx(source: &Source, out_dir: &Path) -> Result<()> {
|
||||
log::info!(action = "Appx"; "Creating StoreLogo.png");
|
||||
resize_and_save_png(source, 50, &out_dir.join("StoreLogo.png"), None)?;
|
||||
resize_and_save_png(source, 50, &out_dir.join("StoreLogo.png"), None, None)?;
|
||||
|
||||
for size in [30, 44, 71, 89, 107, 142, 150, 284, 310] {
|
||||
let file_name = format!("Square{size}x{size}Logo.png");
|
||||
log::info!(action = "Appx"; "Creating {}", file_name);
|
||||
|
||||
resize_and_save_png(source, size, &out_dir.join(&file_name), None)?;
|
||||
resize_and_save_png(source, size, &out_dir.join(&file_name), None, None)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -204,27 +332,34 @@ fn icns(source: &Source, out_dir: &Path) -> Result<()> {
|
||||
|
||||
let mut family = IconFamily::new();
|
||||
|
||||
for (name, entry) in entries {
|
||||
for (_name, entry) in entries {
|
||||
let size = entry.size;
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let image = source.resize_exact(size)?;
|
||||
let image = source.resize_exact(size);
|
||||
|
||||
write_png(image.as_bytes(), &mut buf, size)?;
|
||||
write_png(image.as_bytes(), &mut buf, size).context("failed to write output file")?;
|
||||
|
||||
let image = icns::Image::read_png(&buf[..])?;
|
||||
let image = icns::Image::read_png(&buf[..]).context("failed to read output file")?;
|
||||
|
||||
family
|
||||
.add_icon_with_type(
|
||||
&image,
|
||||
IconType::from_ostype(entry.ostype.parse().unwrap()).unwrap(),
|
||||
)
|
||||
.with_context(|| format!("Can't add {name} to Icns Family"))?;
|
||||
.context("failed to add icon to Icns Family")?;
|
||||
}
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(out_dir.join("icon.icns"))?);
|
||||
family.write(&mut out_file)?;
|
||||
out_file.flush()?;
|
||||
let icns_path = out_dir.join("icon.icns");
|
||||
let mut out_file = BufWriter::new(
|
||||
File::create(&icns_path).fs_context("failed to create output file", &icns_path)?,
|
||||
);
|
||||
family
|
||||
.write(&mut out_file)
|
||||
.fs_context("failed to write output file", &icns_path)?;
|
||||
out_file
|
||||
.flush()
|
||||
.fs_context("failed to flush output file", &icns_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -236,70 +371,54 @@ fn ico(source: &Source, out_dir: &Path) -> Result<()> {
|
||||
let mut frames = Vec::new();
|
||||
|
||||
for size in [32, 16, 24, 48, 64, 256] {
|
||||
let image = source.resize_exact(size)?;
|
||||
let image = source.resize_exact(size);
|
||||
|
||||
// Only the 256px layer can be compressed according to the ico specs.
|
||||
if size == 256 {
|
||||
let mut buf = Vec::new();
|
||||
|
||||
write_png(image.as_bytes(), &mut buf, size)?;
|
||||
write_png(image.as_bytes(), &mut buf, size).context("failed to write output file")?;
|
||||
|
||||
frames.push(IcoFrame::with_encoded(
|
||||
buf,
|
||||
size,
|
||||
size,
|
||||
ExtendedColorType::Rgba8,
|
||||
)?)
|
||||
frames.push(
|
||||
IcoFrame::with_encoded(buf, size, size, ExtendedColorType::Rgba8)
|
||||
.context("failed to create ico frame")?,
|
||||
);
|
||||
} else {
|
||||
frames.push(IcoFrame::as_png(
|
||||
image.as_bytes(),
|
||||
size,
|
||||
size,
|
||||
ExtendedColorType::Rgba8,
|
||||
)?);
|
||||
frames.push(
|
||||
IcoFrame::as_png(image.as_bytes(), size, size, ExtendedColorType::Rgba8)
|
||||
.context("failed to create PNG frame")?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(out_dir.join("icon.ico"))?);
|
||||
let ico_path = out_dir.join("icon.ico");
|
||||
let mut out_file =
|
||||
BufWriter::new(File::create(&ico_path).fs_context("failed to create output file", &ico_path)?);
|
||||
let encoder = IcoEncoder::new(&mut out_file);
|
||||
encoder.encode_images(&frames)?;
|
||||
out_file.flush()?;
|
||||
encoder
|
||||
.encode_images(&frames)
|
||||
.context("failed to encode images")?;
|
||||
out_file
|
||||
.flush()
|
||||
.fs_context("failed to flush output file", &ico_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Generate .png files in 32x32, 64x64, 128x128, 256x256, 512x512 (icon.png)
|
||||
// Main target: Linux
|
||||
fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
|
||||
fn desktop_entries(out_dir: &Path) -> Vec<PngEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for size in [32, 64, 128, 256, 512] {
|
||||
let file_name = match size {
|
||||
256 => "128x128@2x.png".to_string(),
|
||||
512 => "icon.png".to_string(),
|
||||
_ => format!("{size}x{size}.png"),
|
||||
};
|
||||
|
||||
entries.push(PngEntry {
|
||||
out_path: out_dir.join(&file_name),
|
||||
name: file_name,
|
||||
size,
|
||||
});
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn android_entries(out_dir: &Path) -> Result<Vec<PngEntry>> {
|
||||
fn android(
|
||||
source: &Source,
|
||||
input: &Path,
|
||||
manifest: Option<Manifest>,
|
||||
bg_color: &String,
|
||||
out_dir: &Path,
|
||||
) -> Result<()> {
|
||||
fn android_entries(out_dir: &Path) -> Result<AndroidEntries> {
|
||||
struct AndroidEntry {
|
||||
name: &'static str,
|
||||
size: u32,
|
||||
foreground_size: u32,
|
||||
}
|
||||
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let targets = vec![
|
||||
AndroidEntry {
|
||||
name: "hdpi",
|
||||
@@ -327,31 +446,241 @@ fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
|
||||
foreground_size: 432,
|
||||
},
|
||||
];
|
||||
let mut icon_entries = Vec::new();
|
||||
let mut fg_entries = Vec::new();
|
||||
let mut bg_entries = Vec::new();
|
||||
let mut monochrome_entries = Vec::new();
|
||||
|
||||
for target in targets {
|
||||
let folder_name = format!("mipmap-{}", target.name);
|
||||
let out_folder = out_dir.join(&folder_name);
|
||||
|
||||
create_dir_all(&out_folder).context("Can't create Android mipmap output directory")?;
|
||||
create_dir_all(&out_folder).fs_context(
|
||||
"failed to create Android mipmap output directory",
|
||||
&out_folder,
|
||||
)?;
|
||||
|
||||
entries.push(PngEntry {
|
||||
fg_entries.push(PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher_foreground.png"),
|
||||
out_path: out_folder.join("ic_launcher_foreground.png"),
|
||||
size: target.foreground_size,
|
||||
});
|
||||
entries.push(PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher_round.png"),
|
||||
out_path: out_folder.join("ic_launcher_round.png"),
|
||||
size: target.size,
|
||||
icon_entries.push((
|
||||
PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher_round.png"),
|
||||
out_path: out_folder.join("ic_launcher_round.png"),
|
||||
size: target.size,
|
||||
},
|
||||
AndroidIconKind::Rounded,
|
||||
));
|
||||
icon_entries.push((
|
||||
PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher.png"),
|
||||
out_path: out_folder.join("ic_launcher.png"),
|
||||
size: target.size,
|
||||
},
|
||||
AndroidIconKind::Regular,
|
||||
));
|
||||
|
||||
bg_entries.push(PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher_background.png"),
|
||||
out_path: out_folder.join("ic_launcher_background.png"),
|
||||
size: target.foreground_size,
|
||||
});
|
||||
entries.push(PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher.png"),
|
||||
out_path: out_folder.join("ic_launcher.png"),
|
||||
size: target.size,
|
||||
|
||||
monochrome_entries.push(PngEntry {
|
||||
name: format!("{}/{}", folder_name, "ic_launcher_monochrome.png"),
|
||||
out_path: out_folder.join("ic_launcher_monochrome.png"),
|
||||
size: target.foreground_size,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(entries)
|
||||
Ok(AndroidEntries {
|
||||
icon: icon_entries,
|
||||
foreground: fg_entries,
|
||||
background: bg_entries,
|
||||
monochrome: monochrome_entries,
|
||||
})
|
||||
}
|
||||
fn create_color_file(out_dir: &Path, color: &String) -> Result<()> {
|
||||
let values_folder = out_dir.join("values");
|
||||
create_dir_all(&values_folder).fs_context(
|
||||
"Can't create Android values output directory",
|
||||
&values_folder,
|
||||
)?;
|
||||
let launcher_background_xml_path = values_folder.join("ic_launcher_background.xml");
|
||||
let mut color_file = File::create(&launcher_background_xml_path).fs_context(
|
||||
"failed to create Android color file",
|
||||
&launcher_background_xml_path,
|
||||
)?;
|
||||
color_file
|
||||
.write_all(
|
||||
format!(
|
||||
r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">{color}</color>
|
||||
</resources>"#,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.fs_context(
|
||||
"failed to write Android color file",
|
||||
&launcher_background_xml_path,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let android_out = out_dir
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("gen/android/app/src/main/res/");
|
||||
let out = if android_out.exists() {
|
||||
android_out
|
||||
} else {
|
||||
let out = out_dir.join("android");
|
||||
create_dir_all(&out).fs_context("Can't create Android output directory", &out)?;
|
||||
out
|
||||
};
|
||||
let entries = android_entries(&out)?;
|
||||
|
||||
let fg_source = match manifest {
|
||||
Some(ref manifest) => {
|
||||
Some(read_source(input.parent().unwrap().join(
|
||||
manifest.android_fg.as_ref().unwrap_or(&manifest.default),
|
||||
))?)
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
for entry in entries.foreground {
|
||||
log::info!(action = "Android"; "Creating {}", entry.name);
|
||||
resize_and_save_png(
|
||||
fg_source.as_ref().unwrap_or(source),
|
||||
entry.size,
|
||||
&entry.out_path,
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut bg_source = None;
|
||||
let mut has_monochrome_image = false;
|
||||
if let Some(ref manifest) = manifest {
|
||||
if let Some(ref background_path) = manifest.android_bg {
|
||||
let bg = read_source(input.parent().unwrap().join(background_path))?;
|
||||
for entry in entries.background {
|
||||
log::info!(action = "Android"; "Creating {}", entry.name);
|
||||
resize_and_save_png(&bg, entry.size, &entry.out_path, None, None)?;
|
||||
}
|
||||
bg_source.replace(bg);
|
||||
}
|
||||
if let Some(ref monochrome_path) = manifest.android_monochrome {
|
||||
has_monochrome_image = true;
|
||||
let mc = read_source(input.parent().unwrap().join(monochrome_path))?;
|
||||
for entry in entries.monochrome {
|
||||
log::info!(action = "Android"; "Creating {}", entry.name);
|
||||
resize_and_save_png(&mc, entry.size, &entry.out_path, None, None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (entry, kind) in entries.icon {
|
||||
log::info!(action = "Android"; "Creating {}", entry.name);
|
||||
|
||||
let (margin, radius) = match kind {
|
||||
AndroidIconKind::Regular => {
|
||||
let radius = ((entry.size as f32) * 0.0833).round() as u32;
|
||||
(radius, radius)
|
||||
}
|
||||
AndroidIconKind::Rounded => {
|
||||
let margin = ((entry.size as f32) * 0.04).round() as u32;
|
||||
let radius = ((entry.size as f32) * 0.5).round() as u32;
|
||||
(margin, radius)
|
||||
}
|
||||
};
|
||||
|
||||
let image = if let (Some(bg_source), Some(fg_source)) = (bg_source.as_ref(), fg_source.as_ref())
|
||||
{
|
||||
resize_png(
|
||||
fg_source,
|
||||
entry.size,
|
||||
Some(Background::Image(bg_source)),
|
||||
manifest
|
||||
.as_ref()
|
||||
.and_then(|manifest| manifest.android_fg_scale),
|
||||
)?
|
||||
} else {
|
||||
resize_png(source, entry.size, None, None)?
|
||||
};
|
||||
|
||||
let image = apply_round_mask(&image, entry.size, margin, radius);
|
||||
|
||||
let mut out_file = BufWriter::new(
|
||||
File::create(&entry.out_path).fs_context("failed to create output file", &entry.out_path)?,
|
||||
);
|
||||
write_png(image.as_bytes(), &mut out_file, entry.size)
|
||||
.context("failed to write output file")?;
|
||||
out_file
|
||||
.flush()
|
||||
.fs_context("failed to flush output file", &entry.out_path)?;
|
||||
}
|
||||
|
||||
let mut launcher_content = r#"<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>"#
|
||||
.to_owned();
|
||||
|
||||
if bg_source.is_some() {
|
||||
launcher_content
|
||||
.push_str("\n <background android:drawable=\"@mipmap/ic_launcher_background\"/>");
|
||||
} else {
|
||||
create_color_file(&out, bg_color)?;
|
||||
launcher_content
|
||||
.push_str("\n <background android:drawable=\"@color/ic_launcher_background\"/>");
|
||||
}
|
||||
if has_monochrome_image {
|
||||
launcher_content
|
||||
.push_str("\n <monochrome android:drawable=\"@mipmap/ic_launcher_monochrome\"/>");
|
||||
}
|
||||
launcher_content.push_str("\n</adaptive-icon>");
|
||||
|
||||
let any_dpi_folder = out.join("mipmap-anydpi-v26");
|
||||
create_dir_all(&any_dpi_folder).fs_context(
|
||||
"Can't create Android mipmap-anydpi-v26 output directory",
|
||||
&any_dpi_folder,
|
||||
)?;
|
||||
|
||||
let launcher_xml_path = any_dpi_folder.join("ic_launcher.xml");
|
||||
let mut launcher_file = File::create(&launcher_xml_path)
|
||||
.fs_context("failed to create Android launcher file", &launcher_xml_path)?;
|
||||
launcher_file
|
||||
.write_all(launcher_content.as_bytes())
|
||||
.fs_context("failed to write Android launcher file", &launcher_xml_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Generate .png files in 32x32, 64x64, 128x128, 256x256, 512x512 (icon.png)
|
||||
// Main target: Linux
|
||||
fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
|
||||
fn desktop_entries(out_dir: &Path) -> Vec<PngEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for size in [32, 64, 128, 256, 512] {
|
||||
let file_name = match size {
|
||||
256 => "128x128@2x.png".to_string(),
|
||||
512 => "icon.png".to_string(),
|
||||
_ => format!("{size}x{size}.png"),
|
||||
};
|
||||
|
||||
entries.push(PngEntry {
|
||||
out_path: out_dir.join(&file_name),
|
||||
name: file_name,
|
||||
size,
|
||||
});
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn ios_entries(out_dir: &Path) -> Result<Vec<PngEntry>> {
|
||||
@@ -428,20 +757,7 @@ fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
let mut entries = desktop_entries(out_dir);
|
||||
|
||||
let android_out = out_dir
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("gen/android/app/src/main/res/");
|
||||
let out = if android_out.exists() {
|
||||
android_out
|
||||
} else {
|
||||
let out = out_dir.join("android");
|
||||
create_dir_all(&out).context("Can't create Android output directory")?;
|
||||
out
|
||||
};
|
||||
entries.extend(android_entries(&out)?);
|
||||
let entries = desktop_entries(out_dir);
|
||||
|
||||
let ios_out = out_dir
|
||||
.parent()
|
||||
@@ -451,46 +767,221 @@ fn png(source: &Source, out_dir: &Path, ios_color: Rgba<u8>) -> Result<()> {
|
||||
ios_out
|
||||
} else {
|
||||
let out = out_dir.join("ios");
|
||||
create_dir_all(&out).context("Can't create iOS output directory")?;
|
||||
create_dir_all(&out).fs_context("failed to create iOS output directory", &out)?;
|
||||
out
|
||||
};
|
||||
|
||||
for entry in entries {
|
||||
log::info!(action = "PNG"; "Creating {}", entry.name);
|
||||
resize_and_save_png(source, entry.size, &entry.out_path, None)?;
|
||||
resize_and_save_png(source, entry.size, &entry.out_path, None, None)?;
|
||||
}
|
||||
|
||||
for entry in ios_entries(&out)? {
|
||||
log::info!(action = "iOS"; "Creating {}", entry.name);
|
||||
resize_and_save_png(source, entry.size, &entry.out_path, Some(ios_color))?;
|
||||
resize_and_save_png(
|
||||
source,
|
||||
entry.size,
|
||||
&entry.out_path,
|
||||
Some(Background::Color(ios_color)),
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
enum Background<'a> {
|
||||
Color(Rgba<u8>),
|
||||
Image(&'a Source),
|
||||
}
|
||||
|
||||
// Resize image.
|
||||
fn resize_png(
|
||||
source: &Source,
|
||||
size: u32,
|
||||
bg: Option<Background>,
|
||||
scale_percent: Option<f32>,
|
||||
) -> Result<DynamicImage> {
|
||||
let mut image = source.resize_exact(size);
|
||||
|
||||
match bg {
|
||||
Some(Background::Color(bg_color)) => {
|
||||
let mut bg_img = ImageBuffer::from_fn(size, size, |_, _| bg_color);
|
||||
|
||||
let fg = scale_percent
|
||||
.map(|scale| resize_asset(&image, size, scale))
|
||||
.unwrap_or(image);
|
||||
|
||||
image::imageops::overlay(&mut bg_img, &fg, 0, 0);
|
||||
image = bg_img.into();
|
||||
}
|
||||
Some(Background::Image(bg_source)) => {
|
||||
let mut bg = bg_source.resize_exact(size);
|
||||
|
||||
let fg = scale_percent
|
||||
.map(|scale| resize_asset(&image, size, scale))
|
||||
.unwrap_or(image);
|
||||
|
||||
image::imageops::overlay(&mut bg, &fg, 0, 0);
|
||||
image = bg;
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
|
||||
// Resize image and save it to disk.
|
||||
fn resize_and_save_png(
|
||||
source: &Source,
|
||||
size: u32,
|
||||
file_path: &Path,
|
||||
bg_color: Option<Rgba<u8>>,
|
||||
bg: Option<Background>,
|
||||
scale_percent: Option<f32>,
|
||||
) -> Result<()> {
|
||||
let mut image = source.resize_exact(size)?;
|
||||
|
||||
if let Some(bg_color) = bg_color {
|
||||
let mut bg_img = ImageBuffer::from_fn(size, size, |_, _| bg_color);
|
||||
image::imageops::overlay(&mut bg_img, &image, 0, 0);
|
||||
image = bg_img.into();
|
||||
}
|
||||
|
||||
let mut out_file = BufWriter::new(File::create(file_path)?);
|
||||
write_png(image.as_bytes(), &mut out_file, size)?;
|
||||
Ok(out_file.flush()?)
|
||||
let image = resize_png(source, size, bg, scale_percent)?;
|
||||
let mut out_file =
|
||||
BufWriter::new(File::create(file_path).fs_context("failed to create output file", file_path)?);
|
||||
write_png(image.as_bytes(), &mut out_file, size).context("failed to write output file")?;
|
||||
out_file
|
||||
.flush()
|
||||
.fs_context("failed to save output file", file_path)
|
||||
}
|
||||
|
||||
// Encode image data as png with compression.
|
||||
fn write_png<W: Write>(image_data: &[u8], w: W, size: u32) -> Result<()> {
|
||||
fn write_png<W: Write>(image_data: &[u8], w: W, size: u32) -> image::ImageResult<()> {
|
||||
let encoder = PngEncoder::new_with_quality(w, CompressionType::Best, PngFilterType::Adaptive);
|
||||
encoder.write_image(image_data, size, size, ExtendedColorType::Rgba8)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// finds the bounding box of non-transparent pixels in an RGBA image.
|
||||
fn content_bounds(img: &DynamicImage) -> Option<(u32, u32, u32, u32)> {
|
||||
let rgba = img.to_rgba8();
|
||||
let (width, height) = img.dimensions();
|
||||
|
||||
let mut min_x = width;
|
||||
let mut min_y = height;
|
||||
let mut max_x = 0;
|
||||
let mut max_y = 0;
|
||||
let mut found = false;
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let a = rgba.get_pixel(x, y)[3];
|
||||
if a > 0 {
|
||||
found = true;
|
||||
if x < min_x {
|
||||
min_x = x;
|
||||
}
|
||||
if y < min_y {
|
||||
min_y = y;
|
||||
}
|
||||
if x > max_x {
|
||||
max_x = x;
|
||||
}
|
||||
if y > max_y {
|
||||
max_y = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
Some((min_x, min_y, max_x - min_x + 1, max_y - min_y + 1))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn resize_asset(img: &DynamicImage, target_size: u32, scale_percent: f32) -> DynamicImage {
|
||||
let cropped = if let Some((x, y, cw, ch)) = content_bounds(img) {
|
||||
// TODO: Use `&` here instead when we raise MSRV to above 1.79
|
||||
Cow::Owned(img.crop_imm(x, y, cw, ch))
|
||||
} else {
|
||||
Cow::Borrowed(img)
|
||||
};
|
||||
|
||||
let (cw, ch) = cropped.dimensions();
|
||||
let max_dim = cw.max(ch) as f32;
|
||||
let scale = (target_size as f32 * (scale_percent / 100.0)) / max_dim;
|
||||
|
||||
let new_w = (cw as f32 * scale).round() as u32;
|
||||
let new_h = (ch as f32 * scale).round() as u32;
|
||||
|
||||
let resized = resize_image(&cropped, new_w, new_h);
|
||||
|
||||
// Place on transparent square canvas
|
||||
let mut canvas = ImageBuffer::from_pixel(target_size, target_size, Rgba([0, 0, 0, 0]));
|
||||
let offset_x = if new_w > target_size {
|
||||
// Image wider than canvas → start at negative offset
|
||||
-((new_w - target_size) as i32 / 2)
|
||||
} else {
|
||||
(target_size - new_w) as i32 / 2
|
||||
};
|
||||
|
||||
let offset_y = if new_h > target_size {
|
||||
-((new_h - target_size) as i32 / 2)
|
||||
} else {
|
||||
(target_size - new_h) as i32 / 2
|
||||
};
|
||||
|
||||
image::imageops::overlay(&mut canvas, &resized, offset_x.into(), offset_y.into());
|
||||
|
||||
DynamicImage::ImageRgba8(canvas)
|
||||
}
|
||||
|
||||
fn apply_round_mask(
|
||||
img: &DynamicImage,
|
||||
target_size: u32,
|
||||
margin: u32,
|
||||
radius: u32,
|
||||
) -> DynamicImage {
|
||||
// Clamp radius to half of inner size
|
||||
let inner_size = target_size.saturating_sub(2 * margin);
|
||||
let radius = radius.min(inner_size / 2);
|
||||
|
||||
// Resize inner image to fit inside margins
|
||||
let resized = img.resize_exact(inner_size, inner_size, image::imageops::Lanczos3);
|
||||
|
||||
// Prepare output canvas
|
||||
let mut out = ImageBuffer::from_pixel(target_size, target_size, Rgba([0, 0, 0, 0]));
|
||||
|
||||
// Draw the resized image at (margin, margin)
|
||||
image::imageops::overlay(&mut out, &resized, margin as i64, margin as i64);
|
||||
|
||||
// Apply rounded corners
|
||||
for y in 0..target_size {
|
||||
for x in 0..target_size {
|
||||
let inside = if x >= margin + radius
|
||||
&& x < target_size - margin - radius
|
||||
&& y >= margin + radius
|
||||
&& y < target_size - margin - radius
|
||||
{
|
||||
true // inside central rectangle
|
||||
} else {
|
||||
// Determine corner centers
|
||||
let (cx, cy) = if x < margin + radius && y < margin + radius {
|
||||
(margin + radius, margin + radius) // top-left
|
||||
} else if x >= target_size - margin - radius && y < margin + radius {
|
||||
(target_size - margin - radius, margin + radius) // top-right
|
||||
} else if x < margin + radius && y >= target_size - margin - radius {
|
||||
(margin + radius, target_size - margin - radius) // bottom-left
|
||||
} else if x >= target_size - margin - radius && y >= target_size - margin - radius {
|
||||
(target_size - margin - radius, target_size - margin - radius) // bottom-right
|
||||
} else {
|
||||
continue; // edges that are not corners are inside
|
||||
};
|
||||
let dx = x as i32 - cx as i32;
|
||||
let dy = y as i32 - cy as i32;
|
||||
dx * dx + dy * dy <= (radius as i32 * radius as i32)
|
||||
};
|
||||
|
||||
if !inside {
|
||||
out.put_pixel(x, y, Rgba([0, 0, 0, 0]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DynamicImage::ImageRgba8(out)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{SectionItem, Status};
|
||||
#[cfg(windows)]
|
||||
use crate::error::Context;
|
||||
use colored::Colorize;
|
||||
#[cfg(windows)]
|
||||
use serde::Deserialize;
|
||||
@@ -45,7 +47,11 @@ fn build_tools_version() -> crate::Result<Vec<String>> {
|
||||
"json",
|
||||
"-utf8",
|
||||
])
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: "vswhere -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -requires Microsoft.VisualStudio.Component.Windows10SDK.* -format json -utf8".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let output_sdk11 = Command::new(vswhere)
|
||||
.args([
|
||||
@@ -60,19 +66,25 @@ fn build_tools_version() -> crate::Result<Vec<String>> {
|
||||
"json",
|
||||
"-utf8",
|
||||
])
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: "vswhere -prerelease -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -requires Microsoft.VisualStudio.Component.Windows11SDK.* -format json -utf8".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
let mut instances: Vec<VsInstanceInfo> = Vec::new();
|
||||
|
||||
if output_sdk10.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output_sdk10.stdout);
|
||||
let found: Vec<VsInstanceInfo> = serde_json::from_str(&stdout)?;
|
||||
let found: Vec<VsInstanceInfo> =
|
||||
serde_json::from_str(&stdout).context("failed to parse vswhere output")?;
|
||||
instances.extend(found);
|
||||
}
|
||||
|
||||
if output_sdk11.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output_sdk11.stdout);
|
||||
let found: Vec<VsInstanceInfo> = serde_json::from_str(&stdout)?;
|
||||
let found: Vec<VsInstanceInfo> =
|
||||
serde_json::from_str(&stdout).context("failed to parse vswhere output")?;
|
||||
instances.extend(found);
|
||||
}
|
||||
|
||||
@@ -97,7 +109,11 @@ fn webview2_version() -> crate::Result<Option<String>> {
|
||||
let output = Command::new(&powershell_path)
|
||||
.args(["-NoProfile", "-Command"])
|
||||
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
return Ok(Some(
|
||||
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
|
||||
@@ -107,7 +123,11 @@ fn webview2_version() -> crate::Result<Option<String>> {
|
||||
let output = Command::new(&powershell_path)
|
||||
.args(["-NoProfile", "-Command"])
|
||||
.arg("Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: "Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
return Ok(Some(
|
||||
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
|
||||
@@ -117,7 +137,11 @@ fn webview2_version() -> crate::Result<Option<String>> {
|
||||
let output = Command::new(&powershell_path)
|
||||
.args(["-NoProfile", "-Command"])
|
||||
.arg("Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}")
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| crate::error::Error::CommandFailed {
|
||||
command: "Get-ItemProperty -Path 'HKCU:\\SOFTWARE\\Microsoft\\EdgeUpdate\\Clients\\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}' | ForEach-Object {$_.pv}".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
return Ok(Some(
|
||||
String::from_utf8_lossy(&output.stdout).replace('\n', ""),
|
||||
@@ -175,6 +199,22 @@ fn is_xcode_command_line_tools_installed() -> bool {
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn xcode_version() -> Option<String> {
|
||||
Command::new("xcodebuild")
|
||||
.arg("-version")
|
||||
.output()
|
||||
.ok()
|
||||
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
|
||||
.and_then(|s| {
|
||||
s.split('\n')
|
||||
.filter_map(|line| line.strip_prefix("Xcode "))
|
||||
.next()
|
||||
.map(ToString::to_string)
|
||||
})
|
||||
}
|
||||
|
||||
fn de_and_session() -> String {
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
@@ -319,5 +359,11 @@ pub fn items() -> Vec<SectionItem> {
|
||||
}.into()
|
||||
},
|
||||
),
|
||||
#[cfg(target_os = "macos")]
|
||||
SectionItem::new().action(|| {
|
||||
xcode_version().map(|v| (format!("Xcode: {v}"), Status::Success)).unwrap_or_else(|| {
|
||||
(format!("Xcode: {}", "not installed!".red()), Status::Error)
|
||||
}).into()
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::Context,
|
||||
helpers::app_paths::{resolve_frontend_dir, resolve_tauri_dir},
|
||||
Result,
|
||||
};
|
||||
@@ -15,12 +16,12 @@ use std::fmt::{self, Display, Formatter};
|
||||
mod app;
|
||||
mod env_nodejs;
|
||||
mod env_rust;
|
||||
mod env_system;
|
||||
pub mod env_system;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod ios;
|
||||
mod packages_nodejs;
|
||||
mod packages_rust;
|
||||
mod plugins;
|
||||
pub mod plugins;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct JsCliVersionMetadata {
|
||||
@@ -37,7 +38,7 @@ pub struct VersionMetadata {
|
||||
|
||||
fn version_metadata() -> Result<VersionMetadata> {
|
||||
serde_json::from_str::<VersionMetadata>(include_str!("../../metadata-v2.json"))
|
||||
.map_err(Into::into)
|
||||
.context("failed to parse version metadata")
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
@@ -317,8 +318,17 @@ pub fn command(options: Options) -> Result<()> {
|
||||
.extend(app::items(frontend_dir.as_ref(), tauri_dir.as_deref()));
|
||||
|
||||
environment.display();
|
||||
|
||||
packages.display();
|
||||
|
||||
plugins.display();
|
||||
|
||||
if let (Some(frontend_dir), Some(tauri_dir)) = (&frontend_dir, &tauri_dir) {
|
||||
if let Err(error) = plugins::check_mismatched_packages(frontend_dir, tauri_dir) {
|
||||
println!("\n{}: {error}", "Error".bright_red().bold());
|
||||
}
|
||||
}
|
||||
|
||||
app.display();
|
||||
|
||||
// iOS
|
||||
|
||||
@@ -8,7 +8,11 @@ use colored::Colorize;
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::helpers::{cross_command, npm::PackageManager};
|
||||
use crate::error::Context;
|
||||
use crate::{
|
||||
error::Error,
|
||||
helpers::{cross_command, npm::PackageManager},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YarnVersionInfo {
|
||||
@@ -24,10 +28,15 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Opti
|
||||
.arg("info")
|
||||
.arg(name)
|
||||
.args(["version", "--json"])
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn info --json".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let info: YarnVersionInfo = serde_json::from_str(&stdout)?;
|
||||
let info: YarnVersionInfo =
|
||||
serde_json::from_str(&stdout).context("failed to parse yarn info")?;
|
||||
Ok(Some(info.data.last().unwrap().to_string()))
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -41,10 +50,14 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Opti
|
||||
.arg("info")
|
||||
.arg(name)
|
||||
.args(["--fields", "version", "--json"])
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "yarn npm info --fields version --json".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
let info: crate::PackageJson =
|
||||
serde_json::from_reader(std::io::Cursor::new(output.stdout)).unwrap();
|
||||
let info: crate::PackageJson = serde_json::from_reader(std::io::Cursor::new(output.stdout))
|
||||
.context("failed to parse yarn npm info")?;
|
||||
Ok(info.version)
|
||||
} else {
|
||||
Ok(None)
|
||||
@@ -54,7 +67,15 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Opti
|
||||
PackageManager::Npm | PackageManager::Deno | PackageManager::Bun => {
|
||||
let mut cmd = cross_command("npm");
|
||||
|
||||
let output = cmd.arg("show").arg(name).arg("version").output()?;
|
||||
let output = cmd
|
||||
.arg("show")
|
||||
.arg(name)
|
||||
.arg("version")
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "npm show --version".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
Ok(Some(stdout.replace('\n', "")))
|
||||
@@ -65,7 +86,15 @@ pub fn npm_latest_version(pm: &PackageManager, name: &str) -> crate::Result<Opti
|
||||
PackageManager::Pnpm => {
|
||||
let mut cmd = cross_command("pnpm");
|
||||
|
||||
let output = cmd.arg("info").arg(name).arg("version").output()?;
|
||||
let output = cmd
|
||||
.arg("info")
|
||||
.arg(name)
|
||||
.arg("version")
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "pnpm info --version".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
Ok(Some(stdout.replace('\n', "")))
|
||||
@@ -140,12 +169,12 @@ pub fn nodejs_section_item(
|
||||
.unwrap_or_default();
|
||||
|
||||
if version.is_empty() {
|
||||
format!("{} {}: not installed!", package, "".green())
|
||||
format!("{} {}: not installed!", package, " ⱼₛ".black().on_yellow())
|
||||
} else {
|
||||
format!(
|
||||
"{} {}: {}{}",
|
||||
package,
|
||||
"".dimmed(),
|
||||
" ⱼₛ".black().on_yellow(),
|
||||
version,
|
||||
if !(version.is_empty() || latest_ver.is_empty()) {
|
||||
let version = semver::Version::parse(version.as_str()).unwrap();
|
||||
|
||||
@@ -3,14 +3,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{ActionResult, SectionItem};
|
||||
use crate::{
|
||||
helpers::cargo_manifest::{
|
||||
crate_latest_version, crate_version, CargoLock, CargoManifest, CrateVersion,
|
||||
},
|
||||
interface::rust::get_workspace_dir,
|
||||
use crate::helpers::cargo_manifest::{
|
||||
cargo_manifest_and_lock, crate_latest_version, crate_version, CrateVersion,
|
||||
};
|
||||
use colored::Colorize;
|
||||
use std::fs::read_to_string;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<SectionItem> {
|
||||
@@ -18,17 +14,7 @@ pub fn items(frontend_dir: Option<&PathBuf>, tauri_dir: Option<&Path>) -> Vec<Se
|
||||
|
||||
if tauri_dir.is_some() || frontend_dir.is_some() {
|
||||
if let Some(tauri_dir) = tauri_dir {
|
||||
let manifest: Option<CargoManifest> =
|
||||
if let Ok(manifest_contents) = read_to_string(tauri_dir.join("Cargo.toml")) {
|
||||
toml::from_str(&manifest_contents).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let lock: Option<CargoLock> = get_workspace_dir()
|
||||
.ok()
|
||||
.and_then(|p| read_to_string(p.join("Cargo.lock")).ok())
|
||||
.and_then(|s| toml::from_str(&s).ok());
|
||||
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
for dep in ["tauri", "tauri-build", "wry", "tao"] {
|
||||
let crate_version = crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), dep);
|
||||
let item = rust_section_item(dep, crate_version);
|
||||
|
||||
@@ -3,21 +3,107 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::{
|
||||
fs,
|
||||
collections::HashMap,
|
||||
iter,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
helpers::{
|
||||
self,
|
||||
cargo_manifest::{crate_version, CargoLock, CargoManifest},
|
||||
cargo_manifest::{cargo_manifest_and_lock, crate_version},
|
||||
npm::PackageManager,
|
||||
},
|
||||
interface::rust::get_workspace_dir,
|
||||
Error,
|
||||
};
|
||||
|
||||
use super::{packages_nodejs, packages_rust, SectionItem};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstalledPackage {
|
||||
pub crate_name: String,
|
||||
pub npm_name: String,
|
||||
pub crate_version: semver::Version,
|
||||
pub npm_version: semver::Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InstalledPackages(Vec<InstalledPackage>);
|
||||
|
||||
impl InstalledPackages {
|
||||
pub fn mismatched(&self) -> Vec<&InstalledPackage> {
|
||||
self
|
||||
.0
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.crate_version.major != p.npm_version.major || p.crate_version.minor != p.npm_version.minor
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn installed_tauri_packages(
|
||||
frontend_dir: &Path,
|
||||
tauri_dir: &Path,
|
||||
package_manager: PackageManager,
|
||||
) -> InstalledPackages {
|
||||
let know_plugins = helpers::plugins::known_plugins();
|
||||
let crate_names: Vec<String> = iter::once("tauri".to_owned())
|
||||
.chain(
|
||||
know_plugins
|
||||
.keys()
|
||||
.map(|plugin_name| format!("tauri-plugin-{plugin_name}")),
|
||||
)
|
||||
.collect();
|
||||
let npm_names: Vec<String> = iter::once("@tauri-apps/api".to_owned())
|
||||
.chain(
|
||||
know_plugins
|
||||
.keys()
|
||||
.map(|plugin_name| format!("@tauri-apps/plugin-{plugin_name}")),
|
||||
)
|
||||
.collect();
|
||||
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
|
||||
let mut rust_plugins: HashMap<String, semver::Version> = crate_names
|
||||
.iter()
|
||||
.filter_map(|crate_name| {
|
||||
let crate_version =
|
||||
crate_version(tauri_dir, manifest.as_ref(), lock.as_ref(), crate_name).version?;
|
||||
let crate_version = semver::Version::parse(&crate_version)
|
||||
.inspect_err(|_| {
|
||||
// On first run there's no lockfile yet so we get the version requirement from Cargo.toml.
|
||||
// In our templates that's `2` which is not a valid semver version but a version requirement.
|
||||
// log::error confused users so we use log::debug to still be able to see this error if needed.
|
||||
log::debug!("Failed to parse version `{crate_version}` for crate `{crate_name}`");
|
||||
})
|
||||
.ok()?;
|
||||
Some((crate_name.clone(), crate_version))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut npm_plugins = package_manager
|
||||
.current_package_versions(&npm_names, frontend_dir)
|
||||
.unwrap_or_default();
|
||||
|
||||
let installed_plugins = crate_names
|
||||
.iter()
|
||||
.zip(npm_names.iter())
|
||||
.filter_map(|(crate_name, npm_name)| {
|
||||
let (crate_name, crate_version) = rust_plugins.remove_entry(crate_name)?;
|
||||
let (npm_name, npm_version) = npm_plugins.remove_entry(npm_name)?;
|
||||
Some(InstalledPackage {
|
||||
npm_name,
|
||||
npm_version,
|
||||
crate_name,
|
||||
crate_version,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
InstalledPackages(installed_plugins)
|
||||
}
|
||||
|
||||
pub fn items(
|
||||
frontend_dir: Option<&PathBuf>,
|
||||
tauri_dir: Option<&Path>,
|
||||
@@ -27,17 +113,7 @@ pub fn items(
|
||||
|
||||
if tauri_dir.is_some() || frontend_dir.is_some() {
|
||||
if let Some(tauri_dir) = tauri_dir {
|
||||
let manifest: Option<CargoManifest> =
|
||||
if let Ok(manifest_contents) = fs::read_to_string(tauri_dir.join("Cargo.toml")) {
|
||||
toml::from_str(&manifest_contents).ok()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let lock: Option<CargoLock> = get_workspace_dir()
|
||||
.ok()
|
||||
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
|
||||
.and_then(|s| toml::from_str(&s).ok());
|
||||
let (manifest, lock) = cargo_manifest_and_lock(tauri_dir);
|
||||
|
||||
for p in helpers::plugins::known_plugins().keys() {
|
||||
let dep = format!("tauri-plugin-{p}");
|
||||
@@ -67,3 +143,28 @@ pub fn items(
|
||||
|
||||
items
|
||||
}
|
||||
|
||||
pub fn check_mismatched_packages(frontend_dir: &Path, tauri_path: &Path) -> crate::Result<()> {
|
||||
let installed_packages = installed_tauri_packages(
|
||||
frontend_dir,
|
||||
tauri_path,
|
||||
PackageManager::from_project(frontend_dir),
|
||||
);
|
||||
let mismatched_packages = installed_packages.mismatched();
|
||||
if mismatched_packages.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mismatched_text = mismatched_packages
|
||||
.iter()
|
||||
.map(
|
||||
|InstalledPackage {
|
||||
crate_name,
|
||||
crate_version,
|
||||
npm_name,
|
||||
npm_version,
|
||||
}| format!("{crate_name} (v{crate_version}) : {npm_name} (v{npm_version})"),
|
||||
)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
Err(Error::GenericError(format!("Found version mismatched Tauri packages. Make sure the NPM package and Rust crate versions are on the same major/minor releases:\n{mismatched_text}")))
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ use std::{
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
Result,
|
||||
};
|
||||
use clap::Parser;
|
||||
use handlebars::{to_json, Handlebars};
|
||||
use include_dir::{include_dir, Dir};
|
||||
@@ -76,8 +78,10 @@ impl Options {
|
||||
let package_json_path = PathBuf::from(&self.directory).join("package.json");
|
||||
|
||||
let init_defaults = if package_json_path.exists() {
|
||||
let package_json_text = read_to_string(package_json_path)?;
|
||||
let package_json: crate::PackageJson = serde_json::from_str(&package_json_text)?;
|
||||
let package_json_text =
|
||||
read_to_string(&package_json_path).fs_context("failed to read", &package_json_path)?;
|
||||
let package_json: crate::PackageJson =
|
||||
serde_json::from_str(&package_json_text).context("failed to parse JSON")?;
|
||||
let (framework, _) = infer_framework(&package_json_text);
|
||||
InitDefaults {
|
||||
app_name: package_json.product_name.or(package_json.name),
|
||||
@@ -187,7 +191,8 @@ pub fn command(mut options: Options) -> Result<()> {
|
||||
options = options.load()?;
|
||||
|
||||
let template_target_path = PathBuf::from(&options.directory).join("src-tauri");
|
||||
let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata-v2.json"))?;
|
||||
let metadata = serde_json::from_str::<VersionMetadata>(include_str!("../metadata-v2.json"))
|
||||
.context("failed to parse version metadata")?;
|
||||
|
||||
if template_target_path.exists() && !options.force {
|
||||
log::warn!(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::Result;
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
use crate::interface::{AppInterface, AppSettings, Interface};
|
||||
|
||||
@@ -11,16 +11,14 @@ use std::{
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::helpers::config::Config;
|
||||
use anyhow::Context;
|
||||
use crate::{error::Context, helpers::config::Config};
|
||||
use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder};
|
||||
|
||||
pub use rust::{MobileOptions, Options, Rust as AppInterface};
|
||||
pub use rust::{MobileOptions, Options, Rust as AppInterface, WatcherOptions};
|
||||
|
||||
pub trait DevProcess {
|
||||
fn kill(&self) -> std::io::Result<()>;
|
||||
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>>;
|
||||
// TODO:
|
||||
#[allow(unused)]
|
||||
fn wait(&self) -> std::io::Result<ExitStatus>;
|
||||
#[allow(unused)]
|
||||
@@ -31,11 +29,12 @@ pub trait AppSettings {
|
||||
fn get_package_settings(&self) -> tauri_bundler::PackageSettings;
|
||||
fn get_bundle_settings(
|
||||
&self,
|
||||
options: &Options,
|
||||
config: &Config,
|
||||
features: &[String],
|
||||
) -> crate::Result<tauri_bundler::BundleSettings>;
|
||||
fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf>;
|
||||
fn get_binaries(&self) -> crate::Result<Vec<tauri_bundler::BundleBinary>>;
|
||||
fn get_binaries(&self, options: &Options) -> crate::Result<Vec<tauri_bundler::BundleBinary>>;
|
||||
fn app_name(&self) -> Option<String>;
|
||||
fn lib_name(&self) -> Option<String>;
|
||||
|
||||
@@ -52,13 +51,13 @@ pub trait AppSettings {
|
||||
enabled_features.push("default".into());
|
||||
}
|
||||
|
||||
let target: String = if let Some(target) = options.target {
|
||||
let target: String = if let Some(target) = options.target.clone() {
|
||||
target
|
||||
} else {
|
||||
tauri_utils::platform::target_triple()?
|
||||
tauri_utils::platform::target_triple().context("failed to get target triple")?
|
||||
};
|
||||
|
||||
let mut bins = self.get_binaries()?;
|
||||
let mut bins = self.get_binaries(&options)?;
|
||||
if let Some(main_binary_name) = &config.main_binary_name {
|
||||
let main = bins.iter_mut().find(|b| b.main()).context("no main bin?")?;
|
||||
main.set_name(main_binary_name.to_owned());
|
||||
@@ -66,7 +65,7 @@ pub trait AppSettings {
|
||||
|
||||
let mut settings_builder = SettingsBuilder::new()
|
||||
.package_settings(self.get_package_settings())
|
||||
.bundle_settings(self.get_bundle_settings(config, &enabled_features)?)
|
||||
.bundle_settings(self.get_bundle_settings(&options, config, &enabled_features)?)
|
||||
.binaries(bins)
|
||||
.project_out_directory(out_dir)
|
||||
.target(target)
|
||||
@@ -80,7 +79,10 @@ pub trait AppSettings {
|
||||
)
|
||||
}
|
||||
|
||||
settings_builder.build().map_err(Into::into)
|
||||
settings_builder
|
||||
.build()
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,4 +113,9 @@ pub trait Interface: Sized {
|
||||
options: MobileOptions,
|
||||
runner: R,
|
||||
) -> crate::Result<()>;
|
||||
fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
|
||||
&mut self,
|
||||
options: WatcherOptions,
|
||||
runner: R,
|
||||
) -> crate::Result<()>;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use dunce::canonicalize;
|
||||
use glob::glob;
|
||||
use ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||
use notify::RecursiveMode;
|
||||
@@ -29,6 +29,7 @@ use tauri_utils::config::{parse::is_configuration_file, DeepLinkProtocol, Runner
|
||||
|
||||
use super::{AppSettings, DevProcess, ExitReason, Interface};
|
||||
use crate::{
|
||||
error::{Context, Error, ErrorExt},
|
||||
helpers::{
|
||||
app_paths::{frontend_dir, tauri_dir},
|
||||
config::{nsis_settings, reload as reload_config, wix_settings, BundleResources, Config},
|
||||
@@ -54,6 +55,8 @@ pub struct Options {
|
||||
pub args: Vec<String>,
|
||||
pub config: Vec<ConfigValue>,
|
||||
pub no_watch: bool,
|
||||
pub skip_stapling: bool,
|
||||
pub additional_watch_folders: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl From<crate::build::Options> for Options {
|
||||
@@ -66,6 +69,8 @@ impl From<crate::build::Options> for Options {
|
||||
args: options.args,
|
||||
config: options.config,
|
||||
no_watch: true,
|
||||
skip_stapling: options.skip_stapling,
|
||||
additional_watch_folders: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +83,7 @@ impl From<crate::bundle::Options> for Options {
|
||||
target: options.target,
|
||||
features: options.features,
|
||||
no_watch: true,
|
||||
skip_stapling: options.skip_stapling,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -93,6 +99,8 @@ impl From<crate::dev::Options> for Options {
|
||||
args: options.args,
|
||||
config: options.config,
|
||||
no_watch: options.no_watch,
|
||||
skip_stapling: false,
|
||||
additional_watch_folders: options.additional_watch_folders,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -104,6 +112,13 @@ pub struct MobileOptions {
|
||||
pub args: Vec<String>,
|
||||
pub config: Vec<ConfigValue>,
|
||||
pub no_watch: bool,
|
||||
pub additional_watch_folders: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WatcherOptions {
|
||||
pub config: Vec<ConfigValue>,
|
||||
pub additional_watch_folders: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -131,7 +146,14 @@ impl Interface for Rust {
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
watcher.watch(tauri_dir().join("Cargo.toml"), RecursiveMode::NonRecursive)?;
|
||||
watcher
|
||||
.watch(tauri_dir().join("Cargo.toml"), RecursiveMode::NonRecursive)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to watch {}",
|
||||
tauri_dir().join("Cargo.toml").display()
|
||||
)
|
||||
})?;
|
||||
let (manifest, modified) = rewrite_manifest(config)?;
|
||||
if modified {
|
||||
// Wait for the modified event so we don't trigger a re-build later on
|
||||
@@ -148,8 +170,6 @@ impl Interface for Rust {
|
||||
"IPHONEOS_DEPLOYMENT_TARGET",
|
||||
&config.bundle.ios.minimum_system_version,
|
||||
);
|
||||
} else if let Some(minimum_system_version) = &config.bundle.macos.minimum_system_version {
|
||||
std::env::set_var("MACOSX_DEPLOYMENT_TARGET", minimum_system_version);
|
||||
}
|
||||
|
||||
let app_settings = RustAppSettings::new(config, manifest, target)?;
|
||||
@@ -195,8 +215,8 @@ impl Interface for Rust {
|
||||
if options.no_watch {
|
||||
let (tx, rx) = sync_channel(1);
|
||||
self.run_dev(options, run_args, move |status, reason| {
|
||||
on_exit(status, reason);
|
||||
tx.send(()).unwrap();
|
||||
on_exit(status, reason)
|
||||
})?;
|
||||
|
||||
rx.recv().unwrap();
|
||||
@@ -209,7 +229,7 @@ impl Interface for Rust {
|
||||
on_exit(status, reason)
|
||||
})
|
||||
});
|
||||
self.run_dev_watcher(&merge_configs, run)
|
||||
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,12 +251,26 @@ impl Interface for Rust {
|
||||
runner(options)?;
|
||||
Ok(())
|
||||
} else {
|
||||
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
|
||||
let run = Arc::new(|_rust: &mut Rust| runner(options.clone()));
|
||||
self.run_dev_watcher(&merge_configs, run)
|
||||
self.watch(
|
||||
WatcherOptions {
|
||||
config: options.config.clone(),
|
||||
additional_watch_folders: options.additional_watch_folders.clone(),
|
||||
},
|
||||
move || runner(options.clone()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn watch<R: Fn() -> crate::Result<Box<dyn DevProcess + Send>>>(
|
||||
&mut self,
|
||||
options: WatcherOptions,
|
||||
runner: R,
|
||||
) -> crate::Result<()> {
|
||||
let merge_configs = options.config.iter().map(|c| &c.0).collect::<Vec<_>>();
|
||||
let run = Arc::new(|_rust: &mut Rust| runner());
|
||||
self.run_dev_watcher(&options.additional_watch_folders, &merge_configs, run)
|
||||
}
|
||||
|
||||
fn env(&self) -> HashMap<&str, String> {
|
||||
let mut env = HashMap::new();
|
||||
env.insert(
|
||||
@@ -404,20 +438,38 @@ fn dev_options(
|
||||
// Copied from https://github.com/rust-lang/cargo/blob/69255bb10de7f74511b5cef900a9d102247b6029/src/cargo/core/workspace.rs#L665
|
||||
fn expand_member_path(path: &Path) -> crate::Result<Vec<PathBuf>> {
|
||||
let path = path.to_str().context("path is not UTF-8 compatible")?;
|
||||
let res = glob(path).with_context(|| format!("could not parse pattern `{path}`"))?;
|
||||
let res = glob(path).with_context(|| format!("failed to expand glob pattern for {path}"))?;
|
||||
let res = res
|
||||
.map(|p| p.with_context(|| format!("unable to match path to pattern `{path}`")))
|
||||
.map(|p| p.with_context(|| format!("failed to expand glob pattern for {path}")))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn get_watch_folders() -> crate::Result<Vec<PathBuf>> {
|
||||
fn get_watch_folders(additional_watch_folders: &[PathBuf]) -> crate::Result<Vec<PathBuf>> {
|
||||
let tauri_path = tauri_dir();
|
||||
let workspace_path = get_workspace_dir()?;
|
||||
|
||||
// We always want to watch the main tauri folder.
|
||||
let mut watch_folders = vec![tauri_path.to_path_buf()];
|
||||
|
||||
// Add the additional watch folders, resolving the path from the tauri path if it is relative
|
||||
watch_folders.extend(additional_watch_folders.iter().filter_map(|dir| {
|
||||
let path = if dir.is_absolute() {
|
||||
dir.to_owned()
|
||||
} else {
|
||||
tauri_path.join(dir)
|
||||
};
|
||||
|
||||
let canonicalized = canonicalize(&path).ok();
|
||||
if canonicalized.is_none() {
|
||||
log::warn!(
|
||||
"Additional watch folder '{}' not found, ignoring",
|
||||
path.display()
|
||||
);
|
||||
}
|
||||
canonicalized
|
||||
}));
|
||||
|
||||
// We also try to watch workspace members, no matter if the tauri cargo project is the workspace root or a workspace member
|
||||
let cargo_settings = CargoSettings::load(&workspace_path)?;
|
||||
if let Some(members) = cargo_settings.workspace.and_then(|w| w.members) {
|
||||
@@ -480,6 +532,7 @@ impl Rust {
|
||||
|
||||
fn run_dev_watcher<F: Fn(&mut Rust) -> crate::Result<Box<dyn DevProcess + Send>>>(
|
||||
&mut self,
|
||||
additional_watch_folders: &[PathBuf],
|
||||
merge_configs: &[&serde_json::Value],
|
||||
run: Arc<F>,
|
||||
) -> crate::Result<()> {
|
||||
@@ -489,7 +542,7 @@ impl Rust {
|
||||
let (tx, rx) = sync_channel(1);
|
||||
let frontend_path = frontend_dir();
|
||||
|
||||
let watch_folders = get_watch_folders()?;
|
||||
let watch_folders = get_watch_folders(additional_watch_folders)?;
|
||||
|
||||
let common_ancestor = common_path::common_path_all(watch_folders.iter().map(Path::new))
|
||||
.expect("watch_folders should not be empty");
|
||||
@@ -548,7 +601,7 @@ impl Rust {
|
||||
);
|
||||
|
||||
let mut p = process.lock().unwrap();
|
||||
p.kill().with_context(|| "failed to kill app process")?;
|
||||
p.kill().context("failed to kill app process")?;
|
||||
|
||||
// wait for the process to exit
|
||||
// note that on mobile, kill() already waits for the process to exit (duct implementation)
|
||||
@@ -596,18 +649,19 @@ impl<T> MaybeWorkspace<T> {
|
||||
fn resolve(
|
||||
self,
|
||||
label: &str,
|
||||
get_ws_field: impl FnOnce() -> anyhow::Result<T>,
|
||||
) -> anyhow::Result<T> {
|
||||
get_ws_field: impl FnOnce() -> crate::Result<T>,
|
||||
) -> crate::Result<T> {
|
||||
match self {
|
||||
MaybeWorkspace::Defined(value) => Ok(value),
|
||||
MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => {
|
||||
get_ws_field().context(format!(
|
||||
"error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`"
|
||||
))
|
||||
}
|
||||
MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(anyhow::anyhow!(
|
||||
"`workspace=false` is unsupported for `package.{label}`"
|
||||
)),
|
||||
MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: true }) => get_ws_field()
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"error inheriting `{label}` from workspace root manifest's `workspace.package.{label}`"
|
||||
)
|
||||
}),
|
||||
MaybeWorkspace::Workspace(TomlWorkspaceField { workspace: false }) => Err(
|
||||
crate::Error::GenericError("`workspace=false` is unsupported for `package.{label}`".into()),
|
||||
),
|
||||
}
|
||||
}
|
||||
fn _as_defined(&self) -> Option<&T> {
|
||||
@@ -641,11 +695,13 @@ struct WorkspacePackageSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
struct BinarySettings {
|
||||
name: String,
|
||||
/// This is from nightly: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#different-binary-name
|
||||
filename: Option<String>,
|
||||
path: Option<String>,
|
||||
required_features: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
impl BinarySettings {
|
||||
@@ -695,8 +751,11 @@ impl CargoSettings {
|
||||
fn load(dir: &Path) -> crate::Result<Self> {
|
||||
let toml_path = dir.join("Cargo.toml");
|
||||
let toml_str = std::fs::read_to_string(&toml_path)
|
||||
.with_context(|| format!("Failed to read {}", toml_path.display()))?;
|
||||
toml::from_str(&toml_str).with_context(|| format!("Failed to parse {}", toml_path.display()))
|
||||
.fs_context("Failed to read Cargo manifest", toml_path.clone())?;
|
||||
toml::from_str(&toml_str).context(format!(
|
||||
"failed to parse Cargo manifest at {}",
|
||||
toml_path.display()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -728,7 +787,7 @@ pub struct UpdaterConfig {
|
||||
}
|
||||
|
||||
/// Install modes for the Windows update.
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone)]
|
||||
pub enum WindowsUpdateInstallMode {
|
||||
/// Specifies there's a basic UI during the installation process, including a final dialog box at the end.
|
||||
BasicUi,
|
||||
@@ -736,17 +795,12 @@ pub enum WindowsUpdateInstallMode {
|
||||
/// Requires admin privileges if the installer does.
|
||||
Quiet,
|
||||
/// Specifies unattended mode, which means the installation only shows a progress bar.
|
||||
#[default]
|
||||
Passive,
|
||||
// to add more modes, we need to check if the updater relaunch makes sense
|
||||
// i.e. for a full UI mode, the user can also mark the installer to start the app
|
||||
}
|
||||
|
||||
impl Default for WindowsUpdateInstallMode {
|
||||
fn default() -> Self {
|
||||
Self::Passive
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WindowsUpdateInstallMode {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
@@ -789,6 +843,7 @@ impl AppSettings for RustAppSettings {
|
||||
|
||||
fn get_bundle_settings(
|
||||
&self,
|
||||
options: &Options,
|
||||
config: &Config,
|
||||
features: &[String],
|
||||
) -> crate::Result<BundleSettings> {
|
||||
@@ -804,11 +859,10 @@ impl AppSettings for RustAppSettings {
|
||||
.plugins
|
||||
.0
|
||||
.get("updater")
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("failed to get updater configuration: plugins > updater doesn't exist")
|
||||
})?
|
||||
.context("failed to get updater configuration: plugins > updater doesn't exist")?
|
||||
.clone(),
|
||||
)?;
|
||||
)
|
||||
.context("failed to parse updater plugin configuration")?;
|
||||
Some(UpdaterSettings {
|
||||
v1_compatible,
|
||||
pubkey: updater.pubkey,
|
||||
@@ -821,19 +875,22 @@ impl AppSettings for RustAppSettings {
|
||||
let mut settings = tauri_config_to_bundle_settings(
|
||||
self,
|
||||
features,
|
||||
config.identifier.clone(),
|
||||
config,
|
||||
config.bundle.clone(),
|
||||
updater_settings,
|
||||
arch64bits,
|
||||
)?;
|
||||
|
||||
settings.macos.skip_stapling = options.skip_stapling;
|
||||
|
||||
if let Some(plugin_config) = config
|
||||
.plugins
|
||||
.0
|
||||
.get("deep-link")
|
||||
.and_then(|c| c.get("desktop").cloned())
|
||||
{
|
||||
let protocols: DesktopDeepLinks = serde_json::from_value(plugin_config)?;
|
||||
let protocols: DesktopDeepLinks =
|
||||
serde_json::from_value(plugin_config).context("failed to parse desktop deep links from Tauri configuration > plugins > deep-link > desktop")?;
|
||||
settings.deep_link_protocols = Some(match protocols {
|
||||
DesktopDeepLinks::One(p) => vec![p],
|
||||
DesktopDeepLinks::List(p) => p,
|
||||
@@ -864,7 +921,7 @@ impl AppSettings for RustAppSettings {
|
||||
}
|
||||
|
||||
fn app_binary_path(&self, options: &Options) -> crate::Result<PathBuf> {
|
||||
let binaries = self.get_binaries()?;
|
||||
let binaries = self.get_binaries(options)?;
|
||||
let bin_name = binaries
|
||||
.iter()
|
||||
.find(|x| x.main())
|
||||
@@ -890,8 +947,8 @@ impl AppSettings for RustAppSettings {
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
fn get_binaries(&self) -> crate::Result<Vec<BundleBinary>> {
|
||||
let mut binaries: Vec<BundleBinary> = vec![];
|
||||
fn get_binaries(&self, options: &Options) -> crate::Result<Vec<BundleBinary>> {
|
||||
let mut binaries = Vec::new();
|
||||
|
||||
if let Some(bins) = &self.cargo_settings.bin {
|
||||
let default_run = self
|
||||
@@ -900,6 +957,14 @@ impl AppSettings for RustAppSettings {
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
for bin in bins {
|
||||
if let (Some(req_features), Some(opt_features)) =
|
||||
(&bin.required_features, &options.features)
|
||||
{
|
||||
// Check if all required features are enabled.
|
||||
if !req_features.iter().all(|feat| opt_features.contains(feat)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let file_name = bin.file_name();
|
||||
let is_main = file_name == self.cargo_package_settings.name || file_name == default_run;
|
||||
binaries.push(BundleBinary::with_path(
|
||||
@@ -1005,18 +1070,18 @@ impl AppSettings for RustAppSettings {
|
||||
impl RustAppSettings {
|
||||
pub fn new(config: &Config, manifest: Manifest, target: Option<String>) -> crate::Result<Self> {
|
||||
let tauri_dir = tauri_dir();
|
||||
let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load cargo settings")?;
|
||||
let cargo_settings = CargoSettings::load(tauri_dir).context("failed to load Cargo settings")?;
|
||||
let cargo_package_settings = match &cargo_settings.package {
|
||||
Some(package_info) => package_info.clone(),
|
||||
None => {
|
||||
return Err(anyhow::anyhow!(
|
||||
return Err(crate::Error::GenericError(
|
||||
"No package info in the config file".to_owned(),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let ws_package_settings = CargoSettings::load(&get_workspace_dir()?)
|
||||
.context("failed to load cargo settings from workspace root")?
|
||||
.context("failed to load Cargo settings from workspace root")?
|
||||
.workspace
|
||||
.and_then(|v| v.package);
|
||||
|
||||
@@ -1029,7 +1094,7 @@ impl RustAppSettings {
|
||||
ws_package_settings
|
||||
.as_ref()
|
||||
.and_then(|p| p.version.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `version` from workspace"))
|
||||
.context("Couldn't inherit value for `version` from workspace")
|
||||
})
|
||||
.expect("Cargo project does not have a version")
|
||||
});
|
||||
@@ -1049,9 +1114,7 @@ impl RustAppSettings {
|
||||
ws_package_settings
|
||||
.as_ref()
|
||||
.and_then(|v| v.description.clone())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Couldn't inherit value for `description` from workspace")
|
||||
})
|
||||
.context("Couldn't inherit value for `description` from workspace")
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
@@ -1062,9 +1125,7 @@ impl RustAppSettings {
|
||||
ws_package_settings
|
||||
.as_ref()
|
||||
.and_then(|v| v.homepage.clone())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Couldn't inherit value for `homepage` from workspace")
|
||||
})
|
||||
.context("Couldn't inherit value for `homepage` from workspace")
|
||||
})
|
||||
.unwrap()
|
||||
}),
|
||||
@@ -1074,7 +1135,7 @@ impl RustAppSettings {
|
||||
ws_package_settings
|
||||
.as_ref()
|
||||
.and_then(|v| v.authors.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("Couldn't inherit value for `authors` from workspace"))
|
||||
.context("Couldn't inherit value for `authors` from workspace")
|
||||
})
|
||||
.unwrap()
|
||||
}),
|
||||
@@ -1139,16 +1200,20 @@ pub(crate) fn get_cargo_metadata() -> crate::Result<CargoMetadata> {
|
||||
let output = Command::new("cargo")
|
||||
.args(["metadata", "--no-deps", "--format-version", "1"])
|
||||
.current_dir(tauri_dir())
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "cargo metadata --no-deps --format-version 1".to_string(),
|
||||
error,
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"cargo metadata command exited with a non zero exit code: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
));
|
||||
return Err(Error::CommandFailed {
|
||||
command: "cargo metadata".to_string(),
|
||||
error: std::io::Error::other(String::from_utf8_lossy(&output.stderr)),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(serde_json::from_slice(&output.stdout)?)
|
||||
serde_json::from_slice(&output.stdout).context("failed to parse cargo metadata")
|
||||
}
|
||||
|
||||
/// Get the cargo target directory based on the provided arguments.
|
||||
@@ -1156,10 +1221,12 @@ pub(crate) fn get_cargo_metadata() -> crate::Result<CargoMetadata> {
|
||||
/// Otherwise, use the target directory from cargo metadata.
|
||||
pub(crate) fn get_cargo_target_dir(args: &[String]) -> crate::Result<PathBuf> {
|
||||
let path = if let Some(target) = get_cargo_option(args, "--target-dir") {
|
||||
std::env::current_dir()?.join(target)
|
||||
std::env::current_dir()
|
||||
.context("failed to get current directory")?
|
||||
.join(target)
|
||||
} else {
|
||||
get_cargo_metadata()
|
||||
.with_context(|| "failed to get cargo metadata")?
|
||||
.context("failed to run 'cargo metadata' command to get target directory")?
|
||||
.target_directory
|
||||
};
|
||||
|
||||
@@ -1197,7 +1264,7 @@ fn get_cargo_option<'a>(args: &'a [String], option: &'a str) -> Option<&'a str>
|
||||
pub fn get_workspace_dir() -> crate::Result<PathBuf> {
|
||||
Ok(
|
||||
get_cargo_metadata()
|
||||
.context("failed to get cargo metadata")?
|
||||
.context("failed to run 'cargo metadata' command to get workspace directory")?
|
||||
.workspace_root,
|
||||
)
|
||||
}
|
||||
@@ -1221,7 +1288,7 @@ pub fn get_profile_dir(options: &Options) -> &str {
|
||||
fn tauri_config_to_bundle_settings(
|
||||
settings: &RustAppSettings,
|
||||
features: &[String],
|
||||
identifier: String,
|
||||
tauri_config: &Config,
|
||||
config: crate::helpers::config::BundleConfig,
|
||||
updater_config: Option<UpdaterSettings>,
|
||||
arch64bits: bool,
|
||||
@@ -1344,8 +1411,59 @@ fn tauri_config_to_bundle_settings(
|
||||
BundleResources::Map(map) => (None, Some(map)),
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let entitlements = if let Some(plugin_config) = tauri_config
|
||||
.plugins
|
||||
.0
|
||||
.get("deep-link")
|
||||
.and_then(|c| c.get("desktop").cloned())
|
||||
{
|
||||
let protocols: DesktopDeepLinks =
|
||||
serde_json::from_value(plugin_config).context("failed to parse deep link plugin config")?;
|
||||
let domains = match protocols {
|
||||
DesktopDeepLinks::One(protocol) => protocol.domains,
|
||||
DesktopDeepLinks::List(protocols) => protocols.into_iter().flat_map(|p| p.domains).collect(),
|
||||
};
|
||||
|
||||
if domains.is_empty() {
|
||||
config
|
||||
.macos
|
||||
.entitlements
|
||||
.map(PathBuf::from)
|
||||
.map(tauri_bundler::bundle::Entitlements::Path)
|
||||
} else {
|
||||
let mut app_links_entitlements = plist::Dictionary::new();
|
||||
app_links_entitlements.insert(
|
||||
"com.apple.developer.associated-domains".to_string(),
|
||||
domains
|
||||
.into_iter()
|
||||
.map(|domain| format!("applinks:{domain}").into())
|
||||
.collect::<Vec<_>>()
|
||||
.into(),
|
||||
);
|
||||
let entitlements = if let Some(user_provided_entitlements) = config.macos.entitlements {
|
||||
crate::helpers::plist::merge_plist(vec![
|
||||
PathBuf::from(user_provided_entitlements).into(),
|
||||
plist::Value::Dictionary(app_links_entitlements).into(),
|
||||
])?
|
||||
} else {
|
||||
app_links_entitlements.into()
|
||||
};
|
||||
|
||||
Some(tauri_bundler::bundle::Entitlements::Plist(entitlements))
|
||||
}
|
||||
} else {
|
||||
config
|
||||
.macos
|
||||
.entitlements
|
||||
.map(PathBuf::from)
|
||||
.map(tauri_bundler::bundle::Entitlements::Path)
|
||||
};
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
let entitlements = None;
|
||||
|
||||
Ok(BundleSettings {
|
||||
identifier: Some(identifier),
|
||||
identifier: Some(tauri_config.identifier.clone()),
|
||||
publisher: config.publisher,
|
||||
homepage: config.homepage,
|
||||
icon: Some(config.icon),
|
||||
@@ -1354,8 +1472,8 @@ fn tauri_config_to_bundle_settings(
|
||||
copyright: config.copyright,
|
||||
category: match config.category {
|
||||
Some(category) => Some(AppCategory::from_str(&category).map_err(|e| match e {
|
||||
Some(e) => anyhow::anyhow!("invalid category, did you mean `{}`?", e),
|
||||
None => anyhow::anyhow!("invalid category"),
|
||||
Some(e) => Error::GenericError(format!("invalid category, did you mean `{e}`?")),
|
||||
None => Error::GenericError("invalid category".to_string()),
|
||||
})?),
|
||||
None => None,
|
||||
},
|
||||
@@ -1442,16 +1560,27 @@ fn tauri_config_to_bundle_settings(
|
||||
minimum_system_version: config.macos.minimum_system_version,
|
||||
exception_domain: config.macos.exception_domain,
|
||||
signing_identity,
|
||||
skip_stapling: false,
|
||||
hardened_runtime: config.macos.hardened_runtime,
|
||||
provider_short_name,
|
||||
entitlements: config.macos.entitlements,
|
||||
info_plist_path: {
|
||||
entitlements,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
info_plist: None,
|
||||
#[cfg(target_os = "macos")]
|
||||
info_plist: {
|
||||
let mut src_plists = vec![];
|
||||
|
||||
let path = tauri_dir().join("Info.plist");
|
||||
if path.exists() {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
src_plists.push(path.into());
|
||||
}
|
||||
if let Some(info_plist) = &config.macos.info_plist {
|
||||
src_plists.push(info_plist.clone().into());
|
||||
}
|
||||
|
||||
Some(tauri_bundler::bundle::PlistKind::Plist(
|
||||
crate::helpers::plist::merge_plist(src_plists)?,
|
||||
))
|
||||
},
|
||||
},
|
||||
windows: WindowsSettings {
|
||||
@@ -1478,9 +1607,7 @@ fn tauri_config_to_bundle_settings(
|
||||
.cargo_ws_package_settings
|
||||
.as_ref()
|
||||
.and_then(|v| v.license.clone())
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Couldn't inherit value for `license` from workspace")
|
||||
})
|
||||
.context("Couldn't inherit value for `license` from workspace")
|
||||
})
|
||||
.unwrap()
|
||||
})
|
||||
@@ -1542,7 +1669,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parse_cargo_option() {
|
||||
let args = vec![
|
||||
let args = [
|
||||
"build".into(),
|
||||
"--".into(),
|
||||
"--profile".into(),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -11,6 +10,11 @@ use std::{
|
||||
|
||||
use tauri_utils::display_path;
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
Result,
|
||||
};
|
||||
|
||||
struct PathAncestors<'a> {
|
||||
current: Option<&'a Path>,
|
||||
}
|
||||
@@ -57,18 +61,12 @@ impl Config {
|
||||
let mut config = Self::default();
|
||||
|
||||
let get_config = |path: PathBuf| -> Result<ConfigSchema> {
|
||||
let contents = fs::read_to_string(&path).with_context(|| {
|
||||
format!(
|
||||
"failed to read configuration file `{}`",
|
||||
display_path(&path)
|
||||
)
|
||||
})?;
|
||||
toml::from_str(&contents).with_context(|| {
|
||||
format!(
|
||||
"could not parse TOML configuration in `{}`",
|
||||
display_path(&path)
|
||||
)
|
||||
})
|
||||
let contents =
|
||||
fs::read_to_string(&path).fs_context("failed to read configuration file", path.clone())?;
|
||||
toml::from_str(&contents).context(format!(
|
||||
"could not parse TOML configuration in `{}`",
|
||||
display_path(&path)
|
||||
))
|
||||
};
|
||||
|
||||
for current in PathAncestors::new(path) {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use super::{AppSettings, DevProcess, ExitReason, Options, RustAppSettings, RustupTarget};
|
||||
use crate::CommandExt;
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
CommandExt, Error,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use shared_child::SharedChild;
|
||||
use std::{
|
||||
fs,
|
||||
@@ -72,8 +74,7 @@ pub fn run_dev<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
|
||||
dev_cmd.arg("--color");
|
||||
dev_cmd.arg("always");
|
||||
|
||||
// TODO: double check this
|
||||
dev_cmd.stdout(os_pipe::dup_stdout()?);
|
||||
dev_cmd.stdout(os_pipe::dup_stdout().unwrap());
|
||||
dev_cmd.stderr(Stdio::piped());
|
||||
|
||||
dev_cmd.arg("--");
|
||||
@@ -86,16 +87,18 @@ pub fn run_dev<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
|
||||
|
||||
let dev_child = match SharedChild::spawn(&mut dev_cmd) {
|
||||
Ok(c) => Ok(c),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
|
||||
"`{}` command not found.{}",
|
||||
runner,
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => crate::error::bail!(
|
||||
"`{runner}` command not found.{}",
|
||||
if runner == "cargo" {
|
||||
" Please follow the Tauri setup guide: https://v2.tauri.app/start/prerequisites/"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)),
|
||||
Err(e) => Err(e.into()),
|
||||
),
|
||||
Err(e) => Err(Error::CommandFailed {
|
||||
command: runner,
|
||||
error: e,
|
||||
}),
|
||||
}?;
|
||||
let dev_child = Arc::new(dev_child);
|
||||
let dev_child_stderr = dev_child.take_stderr().unwrap();
|
||||
@@ -164,7 +167,8 @@ pub fn build(
|
||||
}
|
||||
|
||||
if options.target == Some("universal-apple-darwin".into()) {
|
||||
std::fs::create_dir_all(&out_dir).with_context(|| "failed to create project out directory")?;
|
||||
std::fs::create_dir_all(&out_dir)
|
||||
.fs_context("failed to create project out directory", out_dir.clone())?;
|
||||
|
||||
let bin_name = bin_path.file_stem().unwrap();
|
||||
|
||||
@@ -189,9 +193,9 @@ pub fn build(
|
||||
|
||||
let lipo_status = lipo_cmd.output_ok()?.status;
|
||||
if !lipo_status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
crate::error::bail!(
|
||||
"Result of `lipo` command was unsuccessful: {lipo_status}. (Is `lipo` installed?)"
|
||||
));
|
||||
);
|
||||
}
|
||||
} else {
|
||||
build_production_app(options, available_targets, config_features)
|
||||
@@ -210,8 +214,8 @@ fn build_production_app(
|
||||
let runner = build_cmd.get_program().to_string_lossy().into_owned();
|
||||
match build_cmd.piped() {
|
||||
Ok(status) if status.success() => Ok(()),
|
||||
Ok(_) => Err(anyhow::anyhow!("failed to build app")),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
|
||||
Ok(_) => crate::error::bail!("failed to build app"),
|
||||
Err(e) if e.kind() == ErrorKind::NotFound => crate::error::bail!(
|
||||
"`{}` command not found.{}",
|
||||
runner,
|
||||
if runner == "cargo" {
|
||||
@@ -219,8 +223,11 @@ fn build_production_app(
|
||||
} else {
|
||||
""
|
||||
}
|
||||
)),
|
||||
Err(e) => Err(e.into()),
|
||||
),
|
||||
Err(e) => Err(Error::CommandFailed {
|
||||
command: runner,
|
||||
error: e,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +309,7 @@ fn validate_target(
|
||||
if let Some(available_targets) = available_targets {
|
||||
if let Some(target) = available_targets.iter().find(|t| t.name == target) {
|
||||
if !target.installed {
|
||||
anyhow::bail!(
|
||||
crate::error::bail!(
|
||||
"Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.",
|
||||
target = target.name,
|
||||
installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::<Vec<&str>>().join(", ")
|
||||
@@ -310,7 +317,7 @@ fn validate_target(
|
||||
}
|
||||
}
|
||||
if !available_targets.iter().any(|t| t.name == target) {
|
||||
anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target);
|
||||
crate::error::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -328,13 +335,7 @@ fn rename_app(
|
||||
""
|
||||
};
|
||||
let new_path = bin_path.with_file_name(format!("{main_binary_name}{extension}"));
|
||||
fs::rename(&bin_path, &new_path).with_context(|| {
|
||||
format!(
|
||||
"failed to rename `{}` to `{}`",
|
||||
tauri_utils::display_path(bin_path),
|
||||
tauri_utils::display_path(&new_path),
|
||||
)
|
||||
})?;
|
||||
fs::rename(&bin_path, &new_path).fs_context("failed to rename app binary", bin_path)?;
|
||||
Ok(new_path)
|
||||
} else {
|
||||
Ok(bin_path)
|
||||
|
||||
@@ -2,18 +2,31 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::Result;
|
||||
use crate::{
|
||||
error::{Error, ErrorExt},
|
||||
Result,
|
||||
};
|
||||
|
||||
use std::{fs::read_dir, path::PathBuf, process::Command};
|
||||
|
||||
pub fn installed_targets() -> Result<Vec<String>> {
|
||||
let output = Command::new("rustc")
|
||||
.args(["--print", "sysroot"])
|
||||
.output()?;
|
||||
.output()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "rustc --print sysroot".to_string(),
|
||||
error,
|
||||
})?;
|
||||
let sysroot_path = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim().to_string());
|
||||
|
||||
let mut targets = Vec::new();
|
||||
for entry in read_dir(sysroot_path.join("lib").join("rustlib"))?.flatten() {
|
||||
for entry in read_dir(sysroot_path.join("lib").join("rustlib"))
|
||||
.fs_context(
|
||||
"failed to read Rust sysroot",
|
||||
sysroot_path.join("lib").join("rustlib"),
|
||||
)?
|
||||
.flatten()
|
||||
{
|
||||
if entry.file_type().map(|t| t.is_dir()).unwrap_or_default() {
|
||||
let name = entry.file_name();
|
||||
if name != "etc" && name != "src" {
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::helpers::{
|
||||
app_paths::tauri_dir,
|
||||
config::{Config, PatternKind},
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{
|
||||
app_paths::tauri_dir,
|
||||
config::{Config, PatternKind},
|
||||
},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use toml_edit::{Array, DocumentMut, InlineTable, Item, TableLike, Value};
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
@@ -84,11 +84,11 @@ fn get_enabled_features(list: &HashMap<String, Vec<String>>, feature: &str) -> V
|
||||
|
||||
pub fn read_manifest(manifest_path: &Path) -> crate::Result<(DocumentMut, String)> {
|
||||
let manifest_str = std::fs::read_to_string(manifest_path)
|
||||
.with_context(|| format!("Failed to read `{manifest_path:?}` file"))?;
|
||||
.fs_context("failed to read Cargo.toml", manifest_path.to_path_buf())?;
|
||||
|
||||
let manifest: DocumentMut = manifest_str
|
||||
.parse::<DocumentMut>()
|
||||
.with_context(|| "Failed to parse Cargo.toml")?;
|
||||
.context("failed to parse Cargo.toml")?;
|
||||
|
||||
Ok((manifest, manifest_str))
|
||||
}
|
||||
@@ -172,10 +172,10 @@ fn write_features<F: Fn(&str) -> bool>(
|
||||
*dep = Value::InlineTable(def);
|
||||
}
|
||||
_ => {
|
||||
return Err(anyhow::anyhow!(
|
||||
crate::error::bail!(
|
||||
"Unsupported {} dependency format on Cargo.toml",
|
||||
dependency_name
|
||||
))
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
@@ -313,10 +313,8 @@ pub fn rewrite_manifest(config: &Config) -> crate::Result<(Manifest, bool)> {
|
||||
let new_manifest_str = serialize_manifest(&manifest);
|
||||
|
||||
if persist && original_manifest_str != new_manifest_str {
|
||||
let mut manifest_file =
|
||||
File::create(&manifest_path).with_context(|| "failed to open Cargo.toml for rewrite")?;
|
||||
manifest_file.write_all(new_manifest_str.as_bytes())?;
|
||||
manifest_file.flush()?;
|
||||
std::fs::write(&manifest_path, new_manifest_str)
|
||||
.fs_context("failed to rewrite Cargo manifest", &manifest_path)?;
|
||||
Ok((
|
||||
Manifest {
|
||||
inner: manifest,
|
||||
|
||||
@@ -10,15 +10,13 @@
|
||||
)]
|
||||
#![cfg(any(target_os = "macos", target_os = "linux", windows))]
|
||||
|
||||
use anyhow::Context;
|
||||
pub use anyhow::Result;
|
||||
|
||||
mod acl;
|
||||
mod add;
|
||||
mod build;
|
||||
mod bundle;
|
||||
mod completions;
|
||||
mod dev;
|
||||
mod error;
|
||||
mod helpers;
|
||||
mod icon;
|
||||
mod info;
|
||||
@@ -34,6 +32,7 @@ mod signer;
|
||||
use clap::{ArgAction, CommandFactory, FromArgMatches, Parser, Subcommand, ValueEnum};
|
||||
use env_logger::fmt::style::{AnsiColor, Style};
|
||||
use env_logger::Builder;
|
||||
pub use error::{Error, ErrorExt, Result};
|
||||
use log::Level;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{BufReader, Write};
|
||||
@@ -48,39 +47,43 @@ use std::{
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use crate::error::Context;
|
||||
|
||||
/// Tauri configuration argument option.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConfigValue(pub(crate) serde_json::Value);
|
||||
|
||||
impl FromStr for ConfigValue {
|
||||
type Err = anyhow::Error;
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(config: &str) -> std::result::Result<Self, Self::Err> {
|
||||
if config.starts_with('{') {
|
||||
Ok(Self(
|
||||
serde_json::from_str(config).context("invalid configuration JSON")?,
|
||||
))
|
||||
Ok(Self(serde_json::from_str(config).with_context(|| {
|
||||
format!("failed to parse config `{config}` as JSON")
|
||||
})?))
|
||||
} else {
|
||||
let path = PathBuf::from(config);
|
||||
if path.exists() {
|
||||
let raw = &read_to_string(&path)
|
||||
.with_context(|| format!("invalid configuration at file {config}"))?;
|
||||
match path.extension() {
|
||||
Some(ext) if ext == "toml" => Ok(Self(::toml::from_str(raw)?)),
|
||||
Some(ext) if ext == "json5" => Ok(Self(::json5::from_str(raw)?)),
|
||||
// treat all other extensions as json
|
||||
_ => Ok(Self(
|
||||
// from tauri-utils/src/config/parse.rs:
|
||||
// we also want to support **valid** json5 in the .json extension
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
match ::json5::from_str(raw) {
|
||||
Ok(json5) => json5,
|
||||
Err(_) => serde_json::from_str(raw)?,
|
||||
},
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("provided configuration path does not exist")
|
||||
let raw =
|
||||
read_to_string(&path).fs_context("failed to read configuration file", path.clone())?;
|
||||
|
||||
match path.extension().and_then(|ext| ext.to_str()) {
|
||||
Some("toml") => Ok(Self(::toml::from_str(&raw).with_context(|| {
|
||||
format!("failed to parse config at {} as TOML", path.display())
|
||||
})?)),
|
||||
Some("json5") => Ok(Self(::json5::from_str(&raw).with_context(|| {
|
||||
format!("failed to parse config at {} as JSON5", path.display())
|
||||
})?)),
|
||||
// treat all other extensions as json
|
||||
_ => Ok(Self(
|
||||
// from tauri-utils/src/config/parse.rs:
|
||||
// we also want to support **valid** json5 in the .json extension
|
||||
// if the json5 is not valid the serde_json error for regular json will be returned.
|
||||
match ::json5::from_str(&raw) {
|
||||
Ok(json5) => json5,
|
||||
Err(_) => serde_json::from_str(&raw)
|
||||
.with_context(|| format!("failed to parse config at {} as JSON", path.display()))?,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +175,13 @@ fn format_error<I: CommandFactory>(err: clap::Error) -> clap::Error {
|
||||
err.format(&mut app)
|
||||
}
|
||||
|
||||
fn get_verbosity(cli_verbose: u8) -> u8 {
|
||||
std::env::var("TAURI_CLI_VERBOSITY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(cli_verbose)
|
||||
}
|
||||
|
||||
/// Run the Tauri CLI with the passed arguments, exiting if an error occurs.
|
||||
///
|
||||
/// The passed arguments should have the binary argument(s) stripped out before being passed.
|
||||
@@ -190,19 +200,7 @@ where
|
||||
A: Into<OsString> + Clone,
|
||||
{
|
||||
if let Err(e) = try_run(args, bin_name) {
|
||||
let mut message = e.to_string();
|
||||
if e.chain().count() > 1 {
|
||||
message.push(':');
|
||||
}
|
||||
e.chain().skip(1).for_each(|cause| {
|
||||
let m = cause.to_string();
|
||||
if !message.contains(&m) {
|
||||
message.push('\n');
|
||||
message.push_str(" - ");
|
||||
message.push_str(&m);
|
||||
}
|
||||
});
|
||||
log::error!("{message}");
|
||||
log::error!("{e}");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
@@ -227,18 +225,24 @@ where
|
||||
Ok(s) => s,
|
||||
Err(e) => e.exit(),
|
||||
};
|
||||
|
||||
let verbosity_number = std::env::var("TAURI_CLI_VERBOSITY")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(cli.verbose);
|
||||
// set the verbosity level so subsequent CLI calls (xcode-script, android-studio-script) refer to it
|
||||
let verbosity_number = get_verbosity(cli.verbose);
|
||||
std::env::set_var("TAURI_CLI_VERBOSITY", verbosity_number.to_string());
|
||||
|
||||
let mut builder = Builder::from_default_env();
|
||||
let init_res = builder
|
||||
if let Err(err) = builder
|
||||
.format_indent(Some(12))
|
||||
.filter(None, verbosity_level(verbosity_number).to_level_filter())
|
||||
// golbin spams an insane amount of really technical logs on the debug level so we're reducing one level
|
||||
.filter(
|
||||
Some("goblin"),
|
||||
verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
|
||||
)
|
||||
// handlebars is not that spammy but its debug logs are typically far from being helpful
|
||||
.filter(
|
||||
Some("handlebars"),
|
||||
verbosity_level(verbosity_number.saturating_sub(1)).to_level_filter(),
|
||||
)
|
||||
.format(|f, record| {
|
||||
let mut is_command_output = false;
|
||||
if let Some(action) = record.key_values().get("action".into()) {
|
||||
@@ -246,7 +250,6 @@ where
|
||||
is_command_output = action == "stdout" || action == "stderr";
|
||||
if !is_command_output {
|
||||
let style = Style::new().fg_color(Some(AnsiColor::Green.into())).bold();
|
||||
|
||||
write!(f, "{style}{action:>12}{style:#} ")?;
|
||||
}
|
||||
} else {
|
||||
@@ -260,15 +263,13 @@ where
|
||||
|
||||
if !is_command_output && log::log_enabled!(Level::Debug) {
|
||||
let style = Style::new().fg_color(Some(AnsiColor::Black.into()));
|
||||
|
||||
write!(f, "[{style}{}{style:#}] ", record.target())?;
|
||||
}
|
||||
|
||||
writeln!(f, "{}", record.args())
|
||||
})
|
||||
.try_init();
|
||||
|
||||
if let Err(err) = init_res {
|
||||
.try_init()
|
||||
{
|
||||
eprintln!("Failed to attach logger: {err}");
|
||||
}
|
||||
|
||||
@@ -301,7 +302,7 @@ fn verbosity_level(num: u8) -> Level {
|
||||
match num {
|
||||
0 => Level::Info,
|
||||
1 => Level::Debug,
|
||||
2.. => Level::Trace,
|
||||
_ => Level::Trace,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,36 +329,51 @@ impl CommandExt for Command {
|
||||
self.stdin(os_pipe::dup_stdin()?);
|
||||
self.stdout(os_pipe::dup_stdout()?);
|
||||
self.stderr(os_pipe::dup_stderr()?);
|
||||
let program = self.get_program().to_string_lossy().into_owned();
|
||||
log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
|
||||
let program = self.get_program().to_string_lossy().into_owned();
|
||||
let args = self
|
||||
.get_args()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
log::debug!(action = "Running"; "Command `{program} {args}`");
|
||||
self.status()
|
||||
}
|
||||
|
||||
fn output_ok(&mut self) -> crate::Result<Output> {
|
||||
let program = self.get_program().to_string_lossy().into_owned();
|
||||
log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
let args = self
|
||||
.get_args()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let cmdline = format!("{program} {args}");
|
||||
log::debug!(action = "Running"; "Command `{cmdline}`");
|
||||
|
||||
self.stdout(Stdio::piped());
|
||||
self.stderr(Stdio::piped());
|
||||
|
||||
let mut child = self.spawn()?;
|
||||
let mut child = self
|
||||
.spawn()
|
||||
.with_context(|| format!("failed to run command `{cmdline}`"))?;
|
||||
|
||||
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 mut line = String::new();
|
||||
let mut lines = stdout_lines_.lock().unwrap();
|
||||
loop {
|
||||
line.clear();
|
||||
match stdout.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stdout"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes().to_vec());
|
||||
if let Ok(mut lines) = stdout_lines_.lock() {
|
||||
loop {
|
||||
line.clear();
|
||||
match stdout.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stdout"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes());
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -367,21 +383,24 @@ impl CommandExt for Command {
|
||||
let stderr_lines_ = stderr_lines.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut line = String::new();
|
||||
let mut lines = stderr_lines_.lock().unwrap();
|
||||
loop {
|
||||
line.clear();
|
||||
match stderr.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stderr"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes().to_vec());
|
||||
if let Ok(mut lines) = stderr_lines_.lock() {
|
||||
loop {
|
||||
line.clear();
|
||||
match stderr.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stderr"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes());
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let status = child.wait()?;
|
||||
let status = child
|
||||
.wait()
|
||||
.with_context(|| format!("failed to run command `{cmdline}`"))?;
|
||||
|
||||
let output = Output {
|
||||
status,
|
||||
@@ -392,7 +411,10 @@ impl CommandExt for Command {
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(anyhow::anyhow!("failed to run {}", program))
|
||||
crate::error::bail!(
|
||||
"failed to run command `{cmdline}`: command exited with status code {}",
|
||||
output.status.code().unwrap_or(-1)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -407,4 +429,10 @@ mod tests {
|
||||
fn verify_cli() {
|
||||
Cli::command().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_output_includes_build() {
|
||||
let help = Cli::command().render_help().to_string();
|
||||
assert!(help.contains("Build"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::Result;
|
||||
use crate::{error::Context, ErrorExt, Result};
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
use tauri_utils::acl::{
|
||||
@@ -22,9 +22,17 @@ pub fn migrate(tauri_dir: &Path) -> Result<MigratedConfig> {
|
||||
{
|
||||
let migrated = migrate_config(&mut config)?;
|
||||
if config_path.extension().is_some_and(|ext| ext == "toml") {
|
||||
fs::write(&config_path, toml::to_string_pretty(&config)?)?;
|
||||
fs::write(
|
||||
&config_path,
|
||||
toml::to_string_pretty(&config).context("failed to serialize config")?,
|
||||
)
|
||||
.fs_context("failed to write config", config_path.clone())?;
|
||||
} else {
|
||||
fs::write(&config_path, serde_json::to_string_pretty(&config)?)?;
|
||||
fs::write(
|
||||
&config_path,
|
||||
serde_json::to_string_pretty(&config).context("failed to serialize config")?,
|
||||
)
|
||||
.fs_context("failed to write config", config_path.clone())?;
|
||||
}
|
||||
|
||||
let mut permissions: Vec<PermissionEntry> = vec!["core:default"]
|
||||
@@ -34,7 +42,10 @@ pub fn migrate(tauri_dir: &Path) -> Result<MigratedConfig> {
|
||||
permissions.extend(migrated.permissions.clone());
|
||||
|
||||
let capabilities_path = config_path.parent().unwrap().join("capabilities");
|
||||
fs::create_dir_all(&capabilities_path)?;
|
||||
fs::create_dir_all(&capabilities_path).fs_context(
|
||||
"failed to create capabilities directory",
|
||||
capabilities_path.clone(),
|
||||
)?;
|
||||
fs::write(
|
||||
capabilities_path.join("migrated.json"),
|
||||
serde_json::to_string_pretty(&Capability {
|
||||
@@ -46,7 +57,12 @@ pub fn migrate(tauri_dir: &Path) -> Result<MigratedConfig> {
|
||||
webviews: vec![],
|
||||
permissions,
|
||||
platforms: None,
|
||||
})?,
|
||||
})
|
||||
.context("failed to serialize capabilities")?,
|
||||
)
|
||||
.fs_context(
|
||||
"failed to write capabilities",
|
||||
capabilities_path.join("migrated.json"),
|
||||
)?;
|
||||
|
||||
return Ok(migrated);
|
||||
@@ -88,7 +104,7 @@ fn migrate_config(config: &mut Value) -> Result<MigratedConfig> {
|
||||
}
|
||||
|
||||
// dangerousUseHttpScheme/useHttpsScheme
|
||||
let dangerouse_use_http = tauri_config
|
||||
let dangerous_use_http = tauri_config
|
||||
.get("security")
|
||||
.and_then(|w| w.as_object())
|
||||
.and_then(|w| {
|
||||
@@ -104,7 +120,7 @@ fn migrate_config(config: &mut Value) -> Result<MigratedConfig> {
|
||||
{
|
||||
for window in windows {
|
||||
if let Some(window) = window.as_object_mut() {
|
||||
window.insert("useHttpsScheme".to_string(), (!dangerouse_use_http).into());
|
||||
window.insert("useHttpsScheme".to_string(), (!dangerous_use_http).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -375,7 +391,8 @@ fn process_security(security: &mut Map<String, Value>) -> Result<()> {
|
||||
let csp = if csp_value.is_null() {
|
||||
csp_value
|
||||
} else {
|
||||
let mut csp: tauri_utils::config_v1::Csp = serde_json::from_value(csp_value)?;
|
||||
let mut csp: tauri_utils::config_v1::Csp =
|
||||
serde_json::from_value(csp_value).context("failed to deserialize CSP")?;
|
||||
match &mut csp {
|
||||
tauri_utils::config_v1::Csp::Policy(csp) => {
|
||||
if csp.contains("connect-src") {
|
||||
@@ -399,7 +416,7 @@ fn process_security(security: &mut Map<String, Value>) -> Result<()> {
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::to_value(csp)?
|
||||
serde_json::to_value(csp).context("failed to serialize CSP")?
|
||||
};
|
||||
|
||||
security.insert("csp".into(), csp);
|
||||
@@ -423,7 +440,8 @@ fn process_allowlist(
|
||||
tauri_config: &mut Map<String, Value>,
|
||||
allowlist: Value,
|
||||
) -> Result<tauri_utils::config_v1::AllowlistConfig> {
|
||||
let allowlist: tauri_utils::config_v1::AllowlistConfig = serde_json::from_value(allowlist)?;
|
||||
let allowlist: tauri_utils::config_v1::AllowlistConfig =
|
||||
serde_json::from_value(allowlist).context("failed to deserialize allowlist")?;
|
||||
|
||||
if allowlist.protocol.asset_scope != Default::default() {
|
||||
let security = tauri_config
|
||||
@@ -435,7 +453,8 @@ fn process_allowlist(
|
||||
let mut asset_protocol = Map::new();
|
||||
asset_protocol.insert(
|
||||
"scope".into(),
|
||||
serde_json::to_value(allowlist.protocol.asset_scope.clone())?,
|
||||
serde_json::to_value(allowlist.protocol.asset_scope.clone())
|
||||
.context("failed to serialize asset scope")?,
|
||||
);
|
||||
if allowlist.protocol.asset {
|
||||
asset_protocol.insert("enable".into(), true.into());
|
||||
@@ -639,7 +658,10 @@ fn allowlist_to_permissions(
|
||||
|
||||
fn process_cli(plugins: &mut Map<String, Value>, cli: Value) -> Result<()> {
|
||||
if let Some(cli) = cli.as_object() {
|
||||
plugins.insert("cli".into(), serde_json::to_value(cli)?);
|
||||
plugins.insert(
|
||||
"cli".into(),
|
||||
serde_json::to_value(cli).context("failed to serialize CLI")?,
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -663,7 +685,10 @@ fn process_updater(
|
||||
.unwrap_or_default()
|
||||
|| updater.get("pubkey").is_some()
|
||||
{
|
||||
plugins.insert("updater".into(), serde_json::to_value(updater)?);
|
||||
plugins.insert(
|
||||
"updater".into(),
|
||||
serde_json::to_value(updater).context("failed to serialize updater")?,
|
||||
);
|
||||
migrated.plugins.insert("updater".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::Context,
|
||||
helpers::{app_paths::walk_builder, npm::PackageManager},
|
||||
Result,
|
||||
Error, ErrorExt, Result,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use itertools::Itertools;
|
||||
use magic_string::MagicString;
|
||||
use oxc_allocator::Allocator;
|
||||
@@ -101,7 +101,8 @@ pub fn migrate(frontend_dir: &Path) -> Result<Vec<String>> {
|
||||
let path = entry.path();
|
||||
let ext = path.extension().unwrap_or_default();
|
||||
if JS_EXTENSIONS.iter().any(|e| e == &ext) {
|
||||
let js_contents = std::fs::read_to_string(path)?;
|
||||
let js_contents =
|
||||
std::fs::read_to_string(path).fs_context("failed to read JS file", path.to_path_buf())?;
|
||||
let new_contents = migrate_imports(
|
||||
path,
|
||||
&js_contents,
|
||||
@@ -110,7 +111,7 @@ pub fn migrate(frontend_dir: &Path) -> Result<Vec<String>> {
|
||||
)?;
|
||||
if new_contents != js_contents {
|
||||
fs::write(path, new_contents)
|
||||
.with_context(|| format!("Error writing {}", path.display()))?;
|
||||
.fs_context("failed to write JS file", path.to_path_buf())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,7 +167,7 @@ fn migrate_imports<'a>(
|
||||
let allocator = Allocator::default();
|
||||
let ret = Parser::new(&allocator, js_source, source_type).parse();
|
||||
if !ret.errors.is_empty() {
|
||||
anyhow::bail!(
|
||||
crate::error::bail!(
|
||||
"failed to parse {} as valid Javascript/Typescript file",
|
||||
path.display()
|
||||
)
|
||||
@@ -193,8 +194,12 @@ fn migrate_imports<'a>(
|
||||
new_module,
|
||||
Default::default(),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to replace import source")?;
|
||||
.map_err(|e| {
|
||||
Error::Context(
|
||||
"failed to replace import source".to_string(),
|
||||
e.to_string().into(),
|
||||
)
|
||||
})?;
|
||||
|
||||
// if module was pluginified, add to packages
|
||||
if let Some(plugin_name) = new_module.strip_prefix("@tauri-apps/plugin-") {
|
||||
@@ -279,8 +284,12 @@ fn migrate_imports<'a>(
|
||||
new_identifier,
|
||||
Default::default(),
|
||||
)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to rename identifier")?;
|
||||
.map_err(|e| {
|
||||
Error::Context(
|
||||
"failed to rename identifier".to_string(),
|
||||
e.to_string().into(),
|
||||
)
|
||||
})?;
|
||||
} else {
|
||||
// if None, we need to remove this specifier,
|
||||
// it will also be replaced with an import from its new plugin below
|
||||
@@ -297,8 +306,12 @@ fn migrate_imports<'a>(
|
||||
|
||||
magic_js_source
|
||||
.remove(script_start + start as i64, script_start + end as i64)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to remove identifier")?;
|
||||
.map_err(|e| {
|
||||
Error::Context(
|
||||
"failed to remove identifier".to_string(),
|
||||
e.to_string().into(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,8 +335,7 @@ fn migrate_imports<'a>(
|
||||
for import in imports_to_add {
|
||||
magic_js_source
|
||||
.append_right(script_start as u32 + start, &import)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to add import")?;
|
||||
.map_err(|e| Error::Context("failed to add import".to_string(), e.to_string().into()))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -331,8 +343,9 @@ fn migrate_imports<'a>(
|
||||
for stmt in stmts_to_add {
|
||||
magic_js_source
|
||||
.append_right(script_start as u32 + start, stmt)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))
|
||||
.context("failed to add statement")?;
|
||||
.map_err(|e| {
|
||||
Error::Context("failed to add statement".to_string(), e.to_string().into())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::ErrorExt,
|
||||
interface::rust::manifest::{read_manifest, serialize_manifest},
|
||||
Result,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use tauri_utils::config_v1::Allowlist;
|
||||
use toml_edit::{DocumentMut, Entry, Item, TableLike, Value};
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn migrate(tauri_dir: &Path) -> Result<()> {
|
||||
migrate_manifest(&mut manifest)?;
|
||||
|
||||
std::fs::write(&manifest_path, serialize_manifest(&manifest))
|
||||
.context("failed to rewrite Cargo manifest")?;
|
||||
.fs_context("failed to rewrite Cargo manifest", &manifest_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::Context,
|
||||
helpers::app_paths::{frontend_dir, tauri_dir},
|
||||
Result,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
mod config;
|
||||
mod frontend;
|
||||
mod manifest;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
error::{Context, ErrorExt},
|
||||
helpers::{
|
||||
app_paths::{frontend_dir, tauri_dir},
|
||||
npm::PackageManager,
|
||||
@@ -13,7 +14,6 @@ use crate::{
|
||||
|
||||
use std::{fs::read_to_string, path::Path};
|
||||
|
||||
use anyhow::Context;
|
||||
use toml_edit::{DocumentMut, Item, Table, TableLike, Value};
|
||||
|
||||
pub fn run() -> Result<()> {
|
||||
@@ -29,7 +29,7 @@ pub fn run() -> Result<()> {
|
||||
migrate_npm_dependencies(frontend_dir)?;
|
||||
|
||||
std::fs::write(&manifest_path, serialize_manifest(&manifest))
|
||||
.context("failed to rewrite Cargo manifest")?;
|
||||
.fs_context("failed to rewrite Cargo manifest", &manifest_path)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -97,14 +97,19 @@ fn migrate_permissions(tauri_dir: &Path) -> Result<()> {
|
||||
];
|
||||
|
||||
for entry in walkdir::WalkDir::new(tauri_dir.join("capabilities")) {
|
||||
let entry = entry?;
|
||||
let entry = entry.map_err(std::io::Error::other).fs_context(
|
||||
"failed to walk capabilities directory",
|
||||
tauri_dir.join("capabilities"),
|
||||
)?;
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "json") {
|
||||
let mut capability = read_to_string(path).context("failed to read capability")?;
|
||||
let mut capability =
|
||||
read_to_string(path).fs_context("failed to read capability", path.to_path_buf())?;
|
||||
for plugin in core_plugins {
|
||||
capability = capability.replace(&format!("\"{plugin}:"), &format!("\"core:{plugin}:"));
|
||||
}
|
||||
std::fs::write(path, capability).context("failed to rewrite capability")?;
|
||||
std::fs::write(path, capability)
|
||||
.fs_context("failed to rewrite capability", path.to_path_buf())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user