Compare commits

..

361 Commits

Author SHA1 Message Date
Xuhui Zheng
926a57bb08 feat(windows): NSIS uninstaller icon and header image support (#15201)
* feat: NSIS uninstaller support.

* chore: fix typo and update schema.

* fix: comments and alias.

* fix: add pages for uninst sidebar.

* chore: fix typo in comments of fields.

* fix: group HEADERIMAGE.

* fix: remove welcome/finish of uninstaller.

* fix: remove uninstaller_sidebar_image.

* Update crates/tauri-utils/src/config.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* chore: revert comments.

* chore: update schema.

* Update crates/tauri-utils/src/config.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* Update crates/tauri-utils/src/config.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* fix: typo of installer_icon.

* chore: add change file.

* fix: typo.

* chore: update config.json.

* Update .changes/feat-uninstaller-icon-image.md

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* fix: revert alias.

* chore: update comments in settings.rs.

* chore: update comments.

* fix: installer.nsi

* fix: installer.nsi

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-04-08 16:10:55 +08:00
signadou
074299c08d feat: add Bring All to Front predefined menu item type (#14307)
* Add Bring All to Front predefined menu item type

* Format and update changefile

* Update .changes/add-bring-all-to-front-predefined-menu-item-type.md
2026-04-07 23:34:01 +08:00
dependabot[bot]
ec5381e951 chore(deps-dev): bump vite from 8.0.0 to 8.0.5 (#15204)
* chore(deps-dev): bump vite from 8.0.0 to 8.0.5

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.0 to 8.0.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Fix audit

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-04-07 12:52:05 +08:00
lanyeeee
b27be063ff feat: add eval_with_callback to Webview and WebviewWindow (#14925)
* feat: add `eval_with_callback` to Webview and WebviewWindow

* docs: fix eval_with_callback docs and add change file

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-04-01 12:14:20 +08:00
Fabian-Lars
cdf5276478 chore(deps): update ctor and napi (#15183) 2026-03-31 12:44:38 +02:00
Amr Bashir
e5b00795c2 feat: cover more cases for data-tauri-drag=region="deep", add example for QA (#15164) 2026-03-28 02:03:30 +02:00
Lucas Fernandes Nogueira
5a0ca7edbb feat(bundler): support Liquid Glass icons, closes #14207 (#14671)
* feat(bundler): support Liquid Glass icons, closes #14207

the `icon` config now supports loading an Assets.car directly or a `.icon` (Icon Composer asset) that gets compiled into an Assets.car file

* fmt

* fix build

* add version checks

* fmt

* fix icns fallback

* fmt
2026-03-26 13:58:58 -03:00
Tony
5dc2cee603 fea(wix): add minimum webview2 version support (#14793)
* feat(wix): add minimum webview2 version option

* Add change file

* Format

* Move comments inside `#if` block

* add breaking change notes

* Add deprecation to description schema

* Merge remote-tracking branch 'upstream/dev' into wix-minimum-webview2-version

* Merge branch 'dev' into wix-minimum-webview2-version
2026-03-26 23:39:05 +08:00
renovate[bot]
3b5b2cc125 chore(deps): update dependency rollup to v4.60.0 (#15154) 2026-03-25 22:12:22 +08:00
Thomas Eizinger
e032c3b342 refactor: replace kuchikiki with dom_query (#14959)
* test: add more unit-tests for `html` module

* refactor: remove html dependencies from `tauri-cli`

* feat: introduce `html-manipulation-2` feature

* Remove deprecation

* Use new feature flag

* Unroll `build` feature

* Introduce `build-2` feature

* Reduce diff

* Use `build-2` in more places

* Add docs

* Refactor `inject_script_hashes`

* Refactor `with_head`

* Rename serialize and parse functions

* Add changes file

* Remove unused function

* Update changelog

* Remove test

* Update wry

* Add todo comments
we don't have the git blame data in html2, better do it now or never
find it again

* refactor `with_head` to `ensure_head`

* Remove unused casts

* Avoid using format to construct html elements
which has the potential to get injected

* Feature gate `inline_isolation`

* Keep old prepends appends

* Fix `inline_isolation_replaces_src_with_content` test

* End meta tag

* Mirror test to old html module

* Use back to `append_html` for csp and link issue

* Try out dom query main branch

* Use nodes instead to avoid an extra clone

* Use wry 0.54.4 and dom_query 0.27

* Mark stability

* Remove `PatternObject`

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-03-25 20:58:51 +08:00
renovate[bot]
386312c73a chore(deps): update dependency rollup to v4.59.1 (#15150)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-24 17:54:31 +01:00
Tony
d34497ef15 refactor(runtime-wry): remove RefCell hack (#14862)
* refactor(runtime-wry): remove RefCell hack

* Remove `Sync` requirement on `on_new_window`

* Merge branch 'dev' into remove-ref-cell-hack

* Add change file
2026-03-24 23:39:50 +08:00
Joshua Megnauth
4017a7ed73 feat: Allow getting inner PathBuf from SafePathBuf (#14908)
* Allow getting inner PathBuf from SafePathBuf

SafePathBuf implements AsRef<Path> which is ergonomic and useful.
However, some APIs take owned PathBufs. This leads to clunky code where
the caller has to get a &Path from the SafePathBuf then take ownership
of that path. Ideally, if a user has a SafePathBuf and needs a PathBuf,
they won't need to allocate again just to get the inner PathBuf back.

* Apply suggestion from @Legend-Master
2026-03-24 16:35:05 +08:00
Lucas Fernandes Nogueira
093e2b47c0 feat(mobile): multi-window support (#14484)
Co-authored-by: FabianLars <30730186+FabianLars@users.noreply.github.com>
2026-03-23 17:32:02 +01:00
renovate[bot]
f0381b4bdf chore(deps): update rust crate tar to v0.4.45 [security] (#15129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-21 10:08:33 +08:00
sftse
d730770bb9 Refactors (#15117)
* refactor(tauri-build): make better use of OsString

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* refactor(tauri-build): dont wrap const value in function

* refactor(tauri-build): None codepath is never used, replace Option<Vec> with Vec

* refactor(tauri): use blocking apis where it makes sense

* refactor(tauri): better use of std::fs API

* refactor(tauri): rewind to start

* refactor(tauri): fmt

* add change file
2026-03-20 18:18:29 +08:00
Seto Elkahfi
80c1425af8 fix(cli): fix ios build when Metal Toolchain exist in the system (#14921) 2026-03-20 09:32:24 +01:00
Fabian-Lars
1ef6a119b1 chore(deps): Update cargo-mobile2 and toml crates (#15115)
* chore(deps): Update cargo-mobile2 and toml crates

* remove toml from tauri-build cargotoml

* 0.22.3

* try a range

* json5 and changefile

* Revert "json5 and changefile"

This reverts commit eda416ba79.

* keep changefile
2026-03-18 18:02:04 +01:00
Fabian-Lars
aabb42f949 chore(deps): update wrangler & flatted to fix audit (#15116) 2026-03-18 09:11:44 +01:00
Sam Lidder
fcb702ec4d fix(cli): allow build --bundles nsis arg in linux+macOS (#14954) 2026-03-17 22:12:44 +01:00
Tony
f17240bf6c refactor: reduce nesting in AppManager::get_asset (#15114) 2026-03-17 18:38:42 +08:00
llogiq
1fa1db5cd9 chore: reduce cloning in EmbeddedAssets::get (#15112)
* reduce cloning in `EmbeddedAssets::get`

* Apply suggestion from @Legend-Master

* Fix github suggestion

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-03-17 12:10:51 +08:00
William Justin
8db451c791 fix(cli): ignore keystore.properties in Android templates (#14996) 2026-03-16 13:09:02 +01:00
renovate[bot]
15b311155f chore(deps): update dependency vite to v8 and @sveltejs/vite-plugin-svelte to v7 (#15103)
* chore(deps): update dependency @sveltejs/vite-plugin-svelte to v7

* Update vite to v8

* Update devalue

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-03-13 20:03:56 +08:00
Tony
812b2990e7 ci: fix duplicated audit runs (#15104) 2026-03-13 19:25:29 +08:00
Shaun Hamilton
c8d7003b23 fix(bundler): set linuxdeploy arch to i386 (#15102)
Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
2026-03-12 22:36:04 +01:00
Eden Lundie
2679ddd5eb fix(ios): fix deadlock during Swift plugin command handling (#15101) 2026-03-12 16:27:49 +01:00
Lucas Nogueira
3f77cc1ee6 fix: drag.js generated CSP regression from #15062 2026-03-10 14:39:01 -03:00
Amr Bashir
2dd9b15a2b feat: add data-tauri-drag-region="deep" (#15062)
* feat: add `data-tauri-drag-region="deep"`

supersedes #6362

* Update data-tauri-drag-region-deep.md

* summary is also clickable

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2026-03-10 09:25:41 -03:00
Amr Bashir
eacd36a4ea refactor(macos-sign): use base64 crate instead of Command (#15038)
* refactor(macos-sign): use base64 crate instead of Command

* add base64 crate as a dependency

* add change file

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2026-03-10 09:14:58 -03:00
Tony
6cb86c9e42 chore(deps): update @rollup/plugin-terser to v1 (#15057) 2026-03-08 10:54:28 +01:00
Fabian-Lars
50833703e4 ci(test-core): split cache per target (#15048) 2026-03-06 13:40:41 +01:00
Tony
8718d08163 enhance(cli): add context before prompting password (#15033)
* enhance(cli): add context before prompting password

* Add change file
2026-03-05 11:38:02 +08:00
Tony
8230973ae8 chore: update js dependencies to fix audit (#15031)
* chore: update js dependencies

* Add serialize-javascript override
2026-03-04 21:10:25 +08:00
Tony
9b17a7aeae fix(ci): bump rustsec/audit-check to v2 and ignore time audit (#15030)
* fix(ci): bump rustsec/audit-check to v2

* Run on pull requests

* Add to ignore list
2026-03-04 18:11:01 +08:00
github-actions[bot]
d86827980d apply version updates (#14897)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-03-04 16:37:29 +08:00
Tony
3a65cc6885 fix(test): disable resolve_resource_dir on Android (#15026) 2026-03-03 22:37:10 +08:00
Tony
52cf195b78 refactor(cli): reduce some nesting code (#14844)
* refactor(cli): reduce some nesting code

* Handle all paths and config file change not first

* Bring back is_ignore
2026-03-03 21:27:55 +08:00
Tony
c3cbff3f74 fix: resource path handles ./ path differently (#14662)
* fix: resource path handles `./` path differently

* Setup CI for tauri utils

* Wrong job name

* Fix tests

* Always run tests and don't run doc tests

* Add change file

* Re-use `test-core` workflow

* Format

* Avoid path clone by calculating target first

* Test tauri-utils first with step instead of matrix

* Use `matrix.platform.command`

* Document `current_dest` and `current_pattern`

* More docs

* Merge remote-tracking branch 'upstream/dev' into refactor-resource-path-iter

* Test with doc tests

* Revert "Test with doc tests"

This reverts commit 388bee9328.

* Merge branch 'dev' into refactor-resource-path-iter

* Merge branch 'dev' into refactor-resource-path-iter
2026-03-03 21:15:44 +08:00
Tony
33754ae5e3 fix(cli): unusable empty password private keys (#15022)
* fix(cli): unusable empty password private keys

* Bump minisign to 0.9 and revert other changes

* Lock to `=0.7.3`
2026-03-03 20:23:50 +08:00
Tony
3935dee121 Add AI tool policy to contributing guide (#15002)
* Add AI tool policy to contrubuting guide

* Apply suggestions from code review

Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>

* nonsense -> nonsensical

---------

Co-authored-by: Fabian-Lars <30730186+FabianLars@users.noreply.github.com>
2026-03-03 17:45:42 +08:00
dependabot[bot]
33932a72b2 chore(deps-dev): bump svelte from 5.51.5 to 5.53.5 (#15015)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.51.5 to 5.53.5.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.53.5/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.53.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-01 11:27:29 +08:00
renovate[bot]
7d3c7593a9 chore(deps): update dependency rollup to v4.59.0 (#15001)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 22:07:20 +08:00
BiggerRain
f20256bca5 chore: fix clippy warnings (#14999) 2026-02-26 14:35:45 +01:00
renovate[bot]
03514414d9 chore(deps): update dependency rollup to v4.58.0 (#14991)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 11:13:03 +08:00
montyc1999
7b16dafb1d fix(tauri-utils): sort csp/plugin/header config maps during codegen so generate_context! is deterministic (#14986)
* fix(tauri-utils): sort csp/plugin/header config maps during codegen so generate_context! is deterministic

* add comments explaining rationale, and todo for removing the hack in v3
2026-02-23 23:14:56 +08:00
dependabot[bot]
7782c5525a chore(deps-dev): bump svelte from 5.35.6 to 5.51.5 (#14973)
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.35.6 to 5.51.5.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.51.5/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.51.5
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-20 21:14:55 +08:00
renovate[bot]
aa0bf8bd19 chore(deps): update eslint monorepo to v10 (major) (#14922)
* chore(deps): update eslint monorepo to v10

* Update typescript-eslint and plugin security
2026-02-20 20:58:41 +08:00
dependabot[bot]
91fb0e161f chore(deps): bump keccak from 0.1.5 to 0.1.6 (#14972)
Bumps [keccak](https://github.com/RustCrypto/sponges) from 0.1.5 to 0.1.6.
- [Commits](https://github.com/RustCrypto/sponges/compare/keccak-v0.1.5...keccak-v0.1.6)

---
updated-dependencies:
- dependency-name: keccak
  dependency-version: 0.1.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-20 19:39:52 +08:00
montyc1999
88c05689c8 fix(deps): disable default-features in objc2 crates (#14967) 2026-02-18 21:31:23 +01:00
Varun Chawla
6252432f07 fix(bundler): swap WIX registry search order to prioritize InstallDir (#14945) 2026-02-16 17:34:58 +01:00
FabianLars
2d0b80eb1c Revert "chore(deps): update rust crate time to v0.3.47 [security] (#14902)"
This reverts commit 86c8c870c8.
2026-02-15 14:19:06 +01:00
renovate[bot]
86c8c870c8 chore(deps): update rust crate time to v0.3.47 [security] (#14902)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-15 14:02:39 +01:00
cui
476e8ee7f3 fix(core): fix content-boundaries start/end order in asset protocol (#14938) 2026-02-15 13:53:01 +01:00
Lucas Fernandes Nogueira
0d1cb83bab fix(cli): missing options on mobile dev/build commands (#14932)
* fix(cli): missing options on mobile dev/build commands

* avoid duplicated flags

* clippy
2026-02-12 21:12:00 -03:00
Lucas Fernandes Nogueira
35c35f27ae fix(cli): features should support a comma separated list (#14931) 2026-02-12 12:24:40 -03:00
renovate[bot]
7d01aa0417 chore(deps): update dependency rollup to v4.57.1 (#14868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-05 23:16:42 +08:00
Tony
7be58a1c64 chore(bundler): bring back binary patching log (#14894)
* chore(bundler): bring back binary patching log

* Fix change tag
2026-02-05 17:59:16 +08:00
dependabot[bot]
06374a902a chore(deps): bump bytes from 1.9.0 to 1.11.1 (#14890)
Bumps [bytes](https://github.com/tokio-rs/bytes) from 1.9.0 to 1.11.1.
- [Release notes](https://github.com/tokio-rs/bytes/releases)
- [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/bytes/compare/v1.9.0...v1.11.1)

---
updated-dependencies:
- dependency-name: bytes
  dependency-version: 1.11.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-04 23:06:29 +08:00
github-actions[bot]
c37368f339 apply version updates (#14884)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-03 11:54:01 -03:00
goosewobbler
06f911aaff fix: don't inherit stdout from parent (#14871) 2026-02-03 11:20:42 -03:00
Lucas Fernandes Nogueira
eb5d88427a fix(codegen): Context generation with custom assets (#14883)
when custom assets are provided (`tauri::generate_context!(assets = my_assets)`) we can't use the fn inner logic directly and capture variables - we must pass them as arguments
2026-02-03 11:01:12 -03:00
FabianLars
540c5b4e59 chore(deps): update wrangler for undici update 2026-02-03 00:56:28 +01:00
FabianLars
5dbb37bab1 chore(api.js): Re-release 2.10.0 as 2.10.1 to fix npm package 2026-02-03 00:32:07 +01:00
FabianLars
19ded696de apply version updates 2026-02-02 23:05:28 +01:00
Fabian-Lars
08558b8ba4 chore(bundler): update gtk3 docs links in code comments (#14872) 2026-02-02 20:15:28 +01:00
Fabian-Lars
ce8fddb464 chore(deps): unlock webkit2gtk patch version (#14873) 2026-02-02 20:15:02 +01:00
Lucas Fernandes Nogueira
517b81e970 chore(api): release 2.10 (#14876)
api, CLI and tauri crates must be in sync
2026-02-02 16:09:22 -03:00
Lucas Fernandes Nogueira
cd68b03ee5 feat(ci): use trusted publishers for NPM publishing (#14874)
* feat(ci): use trusted publishers for NPM publishing

* bump npm version

* update npm

* use empty NODE_AUTH_TOKEN

* entire workflow permissions
2026-02-02 16:09:01 -03:00
github-actions[bot]
8d67af37b6 apply version updates (#14639)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-02-02 09:15:37 -03:00
Tunglies
9f0306fbcc refactor: rewrite some &String to &str (#14857)
* perf(tauri-build): refactor find_icon to use &str to remove unnecessary clones

* refactor(perf-tauri-build): remove obsolete changelog for find_icon refactor

* refactor(tauri-build): inline find_icon logic to simplify window icon path retrieval

* refactor(context): update find_icon predicate to use &str

* refactor(context): simplify predicate in find_icon to accept &&String

* Update crates/tauri-build/src/lib.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-01-31 21:48:00 +08:00
renovate[bot]
f7c083cd41 chore(deps): update dependency rollup to v4.57.0 (#14820) 2026-01-31 20:32:55 +08:00
sftse
32576120fd Fix busy loop (#14839)
* refactor(tauri-cli):  remove unneeded Arc<Mutex>

* fix(tauri-cli): remove busy-looping
2026-01-29 11:13:03 +08:00
sftse
e3fdcb5002 refactor tauri-cli (#14836)
* refactor(tauri-cli): use OsString where possible

* refactor(tauri-cli): remove needless scoping blocks

* refactor(tauri-cli): make return type concrete

* refactor(tauri-cli): use ?

* refactor(tauri-cli): coerce later to trait object

* refactor(tauri-cli): remove clone

* refactor(tauri-cli): make better use of static OnceLock

* fix(tauri-cli): upgrade atomics to SeqCst

* Add change file

* Update .changes/change-pr-14836.md

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-01-29 10:39:00 +08:00
sftse
d453e2e06a refactor(tauri-cli): remove trait implemented only once (#14840) 2026-01-29 10:35:55 +08:00
Fabian-Lars
20b99f9281 refactor: split appimage bundler in multiple files to support new backends (#14841) 2026-01-28 20:27:51 +01:00
sftse
3a4e165b6f Less statics fixup (#14833)
* fix(tauri-cli): be more conservative to preserve behavior (#14804)

* refactor(tauri-cli): move app path initialization into commands
2026-01-27 16:33:11 +08:00
Fabian-Lars
efc4c26ebc chore: fix clippy lints (#14834) 2026-01-26 17:13:08 +01:00
Tony
7fca58230f chore(deps): update nsis_tauri_utils to 0.5.3 (#14830) 2026-01-26 17:55:27 +08:00
Kf637
c769f211fc feat(nsis): add Norwegian language support for installer (#14824)
* feat(nsis): add Norwegian language support for installer

* feat(nsis): add Norwegian language support for installer strings

* Add change file
2026-01-25 16:30:50 +08:00
Oscar Beaumont
4d5d78daf6 fix(specta): don't use #[specta(rename = ...)] with tauri::ipc::Channel (#14812) 2026-01-24 11:00:40 +01:00
renovate[bot]
4794a6ba22 chore(deps): update dependency rollup to v4.55.2 (#14808)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-22 14:09:19 +01:00
dependabot[bot]
09a4e7f55a chore(deps-dev): bump wrangler from 4.20.3 to 4.59.1 (#14806)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 14:06:29 +01:00
Fabian-Lars
c862a0bd8c fix(core): update error wording for invalid version field (#14800)
* fix(core): update error wording for invalid version field

fixes #14799

* fmt
2026-01-21 10:42:15 +08:00
Quentin Goinaud
f82594410c feat(cli): allow electron to start tauri (#13253)
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2026-01-20 22:19:05 +01:00
Ishita Singh
853ed4642f fix(android): improve error handling for external storage file access (#14442)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2026-01-20 21:52:11 +01:00
Tony
53611c4d7b fix(cli): only watch dependent workspace members (#14747)
* fix(cli): only watch dependent workspace members

* Use manifest path instead of `workspace_default_members`

* Add change file

* Merge remote-tracking branch 'upstream/dev' into only-watch-dependencies

* `bug` not `fix`

* Merge branch 'dev' into only-watch-dependencies

* Remove `CargoMetadataExpended`

* Merge remote-tracking branch 'upstream/dev' into only-watch-dependencies

* Remove top level `.taurignore`

* Load ignore files from workspace root
2026-01-20 17:52:34 +08:00
Lucas Fernandes Nogueira
62aa13a124 fix(cli): Android build --apk and --aab flags requiring a value (#14629)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2026-01-19 20:32:43 +01:00
Luke
e919a760ed feat(webview-window): add set_simple_fullscreen to WebviewWindow (#14619)
* feat(webview): add set_simple_fullscreen to WebviewWindow

* add changes

* Combine per platform fn to one

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2026-01-19 11:38:37 +08:00
kandrelczyk
0575dd287e fix(bundler): patch bundle type via string replacement (#14521)
Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: FabianLars <github@fabianlars.de>
2026-01-18 23:51:02 +01:00
Lucas Fernandes Nogueira
eccff97588 fix(cli): possibly empty associated-domains entitlement (#14779) 2026-01-18 19:01:36 +01:00
Tony
08e35fcda0 refactor(cli): remove mutex on config (#14791)
* refactor(cli): remove mutex on config

* Fix ios

* Clippy

* Fix ios

* Unused import

* Fix ios closure and clippy

* Import `ConfigMetadata`

* Remove life time tags
2026-01-18 21:09:36 +08:00
Tony
10a8066db3 refactor(cli): reorder a few parameters (#14792) 2026-01-18 20:57:36 +08:00
Tony
ea31b07f19 fix(cli): inspect's description (#14789) 2026-01-17 19:23:12 +02:00
sftse
7f7d9aac21 Less statics (#14668)
* refactor(tauri-cli): introduce replacement functions

* refactor(tauri-cli): apply replacement to remove.rs

* refactor(tauri-cli): apply replacement to icon.rs

* refactor(tauri-cli): apply replacement to bundle.rs

* refactor(tauri-cli): apply replacement to build.rs

* refactor(tauri-cli): apply replacement to add.rs

* refactor(tauri-cli): apply replacement to dev.rs

* refactor(tauri-cli): less controlflow

* refactor(tauri-cli): split config loading from locking static

* refactor(tauri-cli): remove duplicate checks covered by if let Some(tauri_dir) = tauri_dir

tauri_dir.is_some() must be true, otherwise the entire block is not run, so the frontend_dir check
is irrelevant

* fmt

* refactor(tauri-cli): apply replacement to inspect.rs

* refactor(tauri-cli): dont use statics for config

* refactor(tauri-cli): finish threading directory paths through functions

* fix: clippy

* fixup CI

* refactor(tauri-cli): dont need mutex

* refactor(tauri-cli): rescope mutex use to minimal necessary

* fix CI, reduce mutex use

* fixup PR #14607

* fix: clippy

* refactor(tauri-cli): remove ConfigHandle

* refactor(tauri-cli): remove more unwraps and panicing functions

* refactor(tauri-cli): less mutexes

* refactor(tauri-cli): undo mistaken change, address review comment

* Split android build to 2 functions

* Pass in dirs to migration v1 like the v2 beta

* Add change file

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2026-01-17 23:52:42 +08:00
yy
7873c4a1c6 docs: fix typos in comments (#14787) 2026-01-17 12:30:14 +01:00
Fabian-Lars
123d63a0c1 chore: change webkit2gtk bump to minor 2026-01-15 17:00:58 +01:00
Fabian-Lars
75057c7c08 chore(deps): update wry to 0.54 and webkit2gtk-rs to 2.0.2 (#14778) 2026-01-15 14:48:45 +01:00
sftse
268bb339f0 build(tauri-macos-sign): remove once-cell-regex (#14766)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2026-01-15 13:14:30 +01:00
renovate[bot]
07788af13f chore(deps): update rust crate signal-hook-tokio to 0.4 (#14729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <github@fabianlars.de>
2026-01-15 12:53:18 +01:00
renovate[bot]
9a53c84ec0 chore(deps): update dependency globals to v17 (#14730)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 12:28:04 +01:00
renovate[bot]
137576e8a4 chore(deps): update dependency rollup to v4.55.1 (#14746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-01-15 11:51:21 +01:00
Wuelfhis Asuaje
1b0e335d3f Fix: Updater signer failed signing file without extension (#14713)
* fixing bug where updater signer failed signing file withou extension

* removing  unnecessary unwrap()

* Adding change file. Removing commnent. Adding TODO.

* Apply suggestions from code review

---------

Co-authored-by: Wuelfhis Asuaje <wasuaje@shorecg.com>
Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2026-01-14 09:09:45 +08:00
Amr Bashir
84b04c4a8d fix: fix leftover inconsistent env var in tauri signer sign command (#14759) 2026-01-11 20:37:52 +02:00
Tony
897529d7a2 fix: map rustls-tls to reqwest/rustls-no-provider (#14726)
Co-authored-by: FabianLars <github@fabianlars.de>
2026-01-08 15:14:51 +01:00
dependabot[bot]
3d102e0c13 chore(deps): bump rsa from 0.9.7 to 0.9.10 (#14738)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 19:26:47 +01:00
Fabian-Lars
fea4d02403 chore(deps): update rkyv, closes #14734 (#14736) 2026-01-06 11:16:00 +01:00
Tony
a03219ca19 refactor(cli): disable jsonschema resolving external resources (#14725)
* refactor(cli): disable jsonschema resolving external resources

* Move `CONFIG_SCHEMA_VALIDATOR` to fn

* Format

* Update ureq to fix compile on linux

* New clippy warnings
2026-01-03 19:30:42 +08:00
renovate[bot]
b75ea5bead chore(deps): update rust crate reqwest to 0.13 (#14724)
* chore(deps): update rust crate reqwest to 0.13

* Fix feature name

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2026-01-03 12:34:59 +08:00
Fabian-Lars
dcd1a65889 chore: fix tests (#14720)
* chore: fix tests

* windows
2026-01-02 16:02:23 +01:00
Camilla F
9b242e40c8 fix: BSD support in tauri-runtime (#14700)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-12-29 17:29:07 +01:00
Bruno Verachten
1dbf6fd067 feat(cli): add RISC-V 64-bit pre-built binary support (#14685)
* feat(cli): add RISC-V 64-bit pre-built binary support

Add riscv64gc-unknown-linux-gnu target to the tauri-cli release workflow,
enabling pre-built binaries for RISC-V 64-bit Linux systems.

This eliminates the multi-hour QEMU compilation time that currently blocks
RISC-V adoption of Tauri apps. Native compilation on RISC-V hardware
takes ~63 minutes.

Changes:
- Add RISC-V entry to build matrix with self-hosted runner support
- Support custom `runs_on` field for matrix entries (falls back to `os`)
- Skip dtolnay/rust-toolchain and rust-cache for self-hosted runners
- Source ~/.cargo/env for self-hosted runners where Rust is pre-installed

Tested on:
- Hardware: Banana Pi F3 (RISC-V64, 16GB RAM)
- OS: Debian Trixie (required for WebKit2GTK RISC-V support)
- Build time: 1h 2m 28s
- Binary: ELF 64-bit RISC-V, 16MB stripped

* feat(cli): use cross for RISC-V cross-compilation

Switch from self-hosted runners to cross-rs for building RISC-V binaries.
This approach is simpler and doesn't require maintaining self-hosted infrastructure.

Local testing confirms cross builds a valid RISC-V binary in ~4 minutes.

* refactor(cli): address review feedback for RISC-V workflow

- Skip Rust toolchain and cache setup for cross builds (unnecessary)
- Pin cross version to 0.2.5 for reproducibility
- Fix Linux dependencies condition to match ubuntu-* variants
2025-12-29 10:21:49 -03:00
Tony
8a43e4f9d9 refactor: use u64 instead of usize for nonce gen (#14708) 2025-12-29 08:43:09 -03:00
sftse
a2abe2e6bc refactor(cli): simplify features: Option<Vec<String>> to Vec<String> (#14607)
* refactor: use empty vector for features instead of None

* refactor: reorder

* add change file

* comment: highlight places where serialization is used

* refactor: simplify serialization

* Update .changes/empty-vec-instead-of-none.md

* Update crates/tauri-cli/src/mobile/ios/mod.rs

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-12-29 13:54:51 +08:00
Tony
51f0fcb69c docs: pixel units (#14702) 2025-12-28 10:40:03 +08:00
Tony
0650852d14 docs: things related to WebviewUrl (#14692)
* Typos

* Rename to `handler`/`protocol_handler`

* Fix the `AssetResolver::get` fallback docs

* Refactor and update the docs for `get_url`

* Rename the remaining ones to `get_app_url`

* Apply suggestions from code review

Co-authored-by: Fabian-Lars <github@fabianlars.de>

* Generate schema
2025-12-25 20:05:02 +08:00
Kushal Meghani
c1d82eb3a3 fix(linux): reuse WebContext to prevent WebKitNetworkProcess leak (#14628)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-12-25 13:03:15 +01:00
renovate[bot]
51a0d6d66d chore(deps): update dependency rollup to v4.54.0 (#14688)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-23 22:49:57 +08:00
renovate[bot]
7f48ee9068 chore(deps): update rust crate toml_edit to 0.24 (#14683)
* chore(deps): update rust crate toml_edit to 0.24

* Downgrade indexmap to 2.11.4 for MSRV

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-12-21 22:04:33 +08:00
renovate[bot]
e290642fb4 chore(deps): update dependency rollup to v4.53.5 (#14676)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-19 17:15:17 +08:00
renovate[bot]
b79386010d chore(deps): update dependency rollup to v4.53.4 (#14670)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-18 22:49:52 +08:00
Tony
ff5d76ca21 fix: default WindowConfig::focus to false in Default::default (#14653) 2025-12-14 16:21:44 +08:00
sftse
2d28e3143e Cleanups (#14632)
* refactor(tauri-utils): current_dest and current_pattern always change in-sync, group them to one Option

* refactor(tauri-utils): pass path as explicit argument instead of implicitly through self

* refactor(tauri-utils): remove struct field that is never set to Some

* refactor(tauri-cli): use OsString, OsStr where possible

* refactor(tauri-cli): Deref Arc early

* refactor(tauri-cli): lock config before passing to build::setup()

* refactor(tauri-build, tauri-utils): bettern pattern matching and borrowing

* refactor(tauri-cli): dont need Arc if already have static

* fix(tauri-cli): race condition initializing static flag, remove unnecessary OnceLock

* refactor(tauri-cli): use expect

* refactor(tauri-cli): remove unnecessary OnceLock

* refactor(tauri-cli): better use of dunce api

* refactor(tauri-cli): rename
2025-12-09 21:38:14 +08:00
renovate[bot]
18c69df8c7 chore(deps): update worker-rs crates to 0.7 (#14638)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: FabianLars <github@fabianlars.de>
2025-12-09 13:42:27 +01:00
github-actions[bot]
f2e0405dc2 apply version updates (#14592)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-09 12:41:05 +01:00
FabianLars
54e8d93db1 ci(renovate): group worker-rs updates 2025-12-09 12:11:53 +01:00
Tony
251203b896 fix(linux): work area returns logical rect (#14637) 2025-12-09 18:05:12 +08:00
Tony
91becd9e4f fix(nsis): plugins not signed (#14627) 2025-12-08 20:13:52 +08:00
Tony
018b4db22e fix(bundler): skip signing for nsis uninstaller on --no-sign (#14625) 2025-12-08 20:13:43 +08:00
Tony
731dd5bfdc docs: remove $APP and $LOG from FsScope (#14623) 2025-12-07 23:09:11 +08:00
Tony
7b1b3514df changes(cli): log npm package version parse in debug level (#14621) 2025-12-07 21:06:27 +08:00
sftse
546b296405 fix(tauri-bundler): add a bit more context to error message (#14606) 2025-12-05 11:56:34 +08:00
Tunglies
514cf21e14 chore(deps): update num-bigint-dig to version 0.8.6 (#14591)
* chore(deps): update num-bigint-dig to version 0.8.6

* Update .changes/bump-version-num-bigint-dig.md

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-12-02 10:20:35 +08:00
renovate[bot]
60174527c0 chore(deps): update rust crate ico to 0.5 (#14589)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-02 10:19:09 +08:00
chfaft
4176f93ae4 feat(bundler): consider extensions defined in main.wxs. (#14570)
* feat(bundler): consider extensions defined in main.wxs.

* chore(bundler): apply nitpick and add a change file.

* Update crates/tauri-bundler/src/bundle/windows/msi/mod.rs

chore(bundler): avoid clone and use reference.

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* Update .changes/support-template-extensions.md

chore(bundler): reclassify changes.

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-12-02 09:58:35 +08:00
github-actions[bot]
4408f72af6 apply version updates (#14467)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-30 11:22:38 +01:00
Fabian-Lars
1496145f82 fix(bundler): typo in 32bit arch (#14585)
* fix(bundler): typo in 32bit arch

* changefile
2025-11-30 07:49:33 +01:00
hrzlgnm
f022b2d1ae fix(cli): Skip signing bundles entirely if --no-sign is requested (#14582)
Closes #14581
2025-11-30 11:45:43 +08:00
Fabian-Lars
1573c72402 fix: remove \\r from schema files on windows (#14561) 2025-11-26 11:22:45 +01:00
renovate[bot]
dd7e59a495 chore(deps): update dependency rollup to v4.53.3 (#14519)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-24 13:25:01 +08:00
Fabian-Lars
2d2a1be429 docs(cli): fix formatting of paths 2025-11-20 15:02:46 +01:00
Fabian-Lars
afdd288eab chore(deps): update js-yaml (#14498) 2025-11-19 11:53:13 +01:00
Fabian-Lars
79a7d9ec01 fix(cli): change Cargo.toml version check to debug log (#14468) 2025-11-18 16:08:17 +01:00
Tony
f855caf8a3 fix(cli): mismatched versions check for pnpm (#14481) 2025-11-18 18:16:29 +08:00
Tunglies
ee3cc4a91b perf: remove needless clones in various files for improved performance (#14475)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-17 15:27:49 +01:00
Tony
b5ef603d84 chore(deps): update NSIS to 3.11 (#14478) 2025-11-16 21:56:05 +08:00
Tunglies
ce98d87ce0 refactor: remove needless collect (#14474)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-16 12:49:20 +01:00
Aleksey Ponomarev
ad1dec2e24 fix(core): properly handle async errors in addPluginListener (#14464)
* fix(core): properly handle async errors in addPluginListener

The previous implementation used .then() after invoke() without await, which prevented the catch block from handling rejected promises. Now using await to properly catch errors and allow fallback to camelCase registerListener method.

* Change file and generate `bundle.global.js`

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-11-15 12:07:51 +08:00
renovate[bot]
beffcd880f chore(deps): update dependency rollup to v4.53.2 (#14459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-15 10:23:57 +08:00
github-actions[bot]
956031d73d apply version updates (#14458)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-13 09:14:14 -03:00
Lucas Fernandes Nogueira
4b00130b86 refactor(core): improve iOS log messages from stdout/stderr (#14385)
* refactor(core): improve iOS log messages from stdout/stderr

move the stdout/stderr forward logic to Swift so it does not consume a Rust thread and never deadlocks on the simulator

I had to work on this because i'm facing #12172 again on latest Xcode (26.1)

* patch
2025-11-13 08:18:50 -03:00
Tony
8e3bd63db9 perf(codegen): wrap generated context in a fn (#14457)
* perf(codegen): wrap generated context in a fn

* Add comment about the reasoning
2025-11-13 15:24:10 +08:00
renovate[bot]
cfe47871a5 chore(deps): update dependency rollup to v4.53.1 (#14444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-12 22:05:17 +08:00
Fabian-Lars
236f55b7aa docs: enable dynamic-acl feature on docs.rs (#14452) 2025-11-12 10:21:18 +01:00
github-actions[bot]
9bb7e79e97 apply version updates (#14425)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-09 12:48:27 +01:00
FabianLars
d566679a99 ci: don't re-generate lockfile on prepublish 2025-11-09 12:08:57 +01:00
Kushal Meghani
3899d456d4 Address review comments (#14426)
* Address review comments

* Revert comments in `impl FromStr for ConfigValue`
2025-11-07 09:38:05 +08:00
Tony
b586ecf1f4 fix(cli): demultiply tiny skia pixels (#14416)
* fix(cli): demultiply tiny skia pixels

* Pull resize out to a function `resize_image`

* Move comments as well

* Use cow for older rust versions
2025-11-06 10:12:10 +08:00
Fabian-Lars
dd70d213cd chore(deps): update minisign to 0.8 (#14415) 2025-11-05 14:58:54 +01:00
Kushal Meghani
d06a1994e9 refactor: improve cli code readability (#14333) 2025-11-05 13:48:32 +01:00
github-actions[bot]
b446a858de apply version updates (#14409)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-11-04 17:00:19 +01:00
renovate[bot]
85ba5315c2 chore(deps): update dependency @types/node to v24 (#14376)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 11:27:01 +01:00
Adam
779612ac84 fix(cli): respect required-features field from Cargo.toml (#14379)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-11-04 11:16:01 +01:00
Fabian-Lars
22edc65aad fix(bundler/cli): set user-agent when fetching build tools (#14408) 2025-11-04 10:53:44 +01:00
Tony
9a19226369 fix(nsis): uninstall fails when manually close app on kill app dialog (#14410) 2025-11-04 17:18:21 +08:00
Chase Knowlden
fd8c30b4f1 fix: premultiply alpha before resizing (fix #14351) (#14353)
* fix: Premultiply alpha before resizing

* feat: Use rayon for process speedup

* Fix change tag

* `cargo fmt`

* Document reasoning & use imageops::resize directly

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-11-04 11:16:11 +08:00
renovate[bot]
18464d9481 chore(deps): update dependency vitest to v4 (#14361)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-02 17:21:40 +08:00
Tony
b80f9deb5f chore: fix new clippy warnings (derive default) (#14395)
* chore: fix new clippy warnings (derive default)

* Fix left over `#[cfg(feature = "isolation")]`
2025-10-31 21:12:41 +08:00
Sebastian Neubauer
1afa9df6d5 fix(tauri-utils): Use write_if_changed more (#13621)
Replace `fs::write` with `write_if_changed` in two places. This can
prevent unnecessary rebuilds. (I didn’t encounter any, but this should
be ok nonetheless.)
2025-10-31 09:19:35 +08:00
Fabian-Lars
75a1fec705 ci: don't cache pnpm files in version-or-publish workflow (#14392) 2025-10-30 10:25:12 +01:00
github-actions[bot]
100dc94c48 apply version updates (#14378)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-29 15:15:51 +01:00
Fabian-Lars
7f710b8f3b fix(bundler): inline linuxdeploy plugin scripts (#14390) 2025-10-29 14:50:33 +01:00
Braden Wong
bda1d22369 docs(webviewWindow): fix incorrect import in JSDoc example (#14388)
The getByLabel method is a static method on WebviewWindow, not Webview.
Updated the JSDoc example to import and use the correct class name.
2025-10-29 15:34:16 +08:00
Tony
28b9e7c7b8 fix: throw on custom protocol IPC fails (#14377) 2025-10-28 18:07:50 +08:00
renovate[bot]
3056d44d96 chore(deps): update dependency @rollup/plugin-typescript to v12.3.0 (#14364)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-28 15:57:47 +08:00
kandrelczyk
fc017ee257 add info to error message (fix 14186) (#14368)
* add info to error message

* changes file and  linux only warning

Signed-off-by: Krzysztof Andrelczyk <cristof@curiana.net>

* Update change file

---------

Signed-off-by: Krzysztof Andrelczyk <cristof@curiana.net>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-10-28 15:03:48 +08:00
github-actions[bot]
67c7418c06 apply version updates (#14348)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-22 13:38:43 +02:00
Fabian-Lars
f59bf9d539 chore: Add missing changefile for tauri-macos-sign (#14337) 2025-10-22 12:28:10 +02:00
Fabian-Lars
4b6b8690ab chore: remove --cfg docsrs to fix docs.rs builds (#14347) 2025-10-22 11:33:30 +02:00
renovate[bot]
cdc5594286 chore(deps): update dependency rollup to v4.52.5 (#14339)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-21 17:09:40 +08:00
dependabot[bot]
a1c231ec29 chore(deps-dev): bump vite from 7.1.5 to 7.1.11 (#14336)
* chore(deps-dev): bump vite from 7.1.5 to 7.1.11

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.1.5 to 7.1.11.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.1.11/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.1.11
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Dedupe

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-10-21 16:30:28 +08:00
Tony
752c923002 chore: fix some typos (#14334) 2025-10-20 22:51:28 +08:00
github-actions[bot]
cb28f4368c apply version updates (#14137)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-20 09:49:46 -03:00
Lucas Nogueira
6aa7f2d852 chore(deps): minor bump plugin, codegen and macros crates 2025-10-20 09:25:07 -03:00
Lucas Nogueira
06f26bbb24 chore(deps): update tao to 0.34.5 2025-10-20 09:20:13 -03:00
Lucas Fernandes Nogueira
68cb318979 feat(core): add stop, restart, destroy and configuration changed Android hooks (#14328)
* feat(core): add pause, destroy and configuration changed Android hooks

* Apply suggestions from code review
2025-10-20 08:49:26 -03:00
Lucas Fernandes Nogueira
3397fd9bfe feat(core): back button event on Android, closes #8142 (#14133)
* feat(core): back button event and exit on Android, closes #8142

I've used https://github.com/ionic-team/capacitor-plugins/blob/main/app/android/src/main/java/com/capacitorjs/plugins/app/AppPlugin.java as a reference here, checking if there's a back button event handler with a default of webview's goBack implementation

* missing change file

* remove exit impl

* fmt

* update wry

* fix default back press

* add remove_listener
2025-10-15 20:50:15 -03:00
Bipin Pandey
3b4fac2017 feat(android): add auto_increment_version_code option for Android builds (#14194)
* add new api (auto_increment_version_code) in android configuration

* ensure increment is only ran once

* skip on dev

* update doc

* change file

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-10-14 15:01:54 -03:00
Felix Häcker
684791efa6 fix(macos): Always try to create webview, even if webkit runtime isn't detected correctly (#14276) 2025-10-14 11:58:43 -03:00
Lucas Fernandes Nogueira
25e920e169 fix(cli): wait for dev command to exit with --no-watch, closes #14284 (#14298) 2025-10-14 07:28:18 -03:00
Lucas Nogueira
a279485856 chore(cli): update cargo-mobile2 to 0.21.1
applies https://github.com/tauri-apps/cargo-mobile2/pull/491
2025-10-10 13:44:33 -03:00
Lucas Fernandes Nogueira
7b0d4e7322 fix(core): SHA256 hash for JS scripts CSP on Windows (#14265)
* fix(core): SHA256 hash for JS scripts CSP on Windows

we hash JS scripts as SHA256 for the Content-Security-Policy (CSP) header. The isolation pattern is broken on Windows due to the hash including carriage return characters, which are not processed when the webview checks the script hash to see if the CSP allows the script.

* fmt, clippy
2025-10-10 08:11:38 -03:00
Tony
c5008b829d fix: skip empty script tag for CSP hash properly (#14274)
* fix: skip empty script tag for CSP hash properly

* add change file

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-10-10 08:11:08 -03:00
Lucas Fernandes Nogueira
b5aa018702 feat(cli): update cargo-mobile2 to 0.21, closes #14238 (#14268) 2025-10-09 08:30:36 -03:00
Tony
55453e8453 enhance(cli): check mismatched packages in info (#14262) 2025-10-08 19:53:55 +08:00
Lucas Fernandes Nogueira
75082cc5b3 feat(cli): add mobile run commands, closes #13196 (#14120)
* feat(cli): add mobile run commands, closes #13196

* headers

* debug by default

* fix android env

* implement watcher

* clippy

* skip ipa build
2025-10-08 07:58:17 -03:00
Lucas Fernandes Nogueira
006d592837 fix(core): parse Android plugin args starting with is, closes #14254 (#14260)
by default Jackson treats the `isX` as a getter, so it looks for the `x` key in the JSON. To match behavior on other platforms we now configure Jackson to treat it as the field name itself.
2025-10-08 07:53:35 -03:00
Tony
d2938486e9 fix(cli): js icon in tauri info (#14261) 2025-10-08 17:58:47 +08:00
DomanskiFilip
19fb6f7cb0 fix(cli): improve Android BuildTask.kt Windows executable detection for nvm4w Fixes #13892 (#14146)
* fix(cli): improve Android BuildTask.kt Windows executable detection

- Fix Android build error on Windows when using nvm4w
- Add robust fallback logic for Windows executable detection
- Prevent 'node.exe.cmd' and 'Cannot find module' errors
- Graceful fallback to cargo when Node.js detection fails

Fixes #13892

* strip extension from project, try exe/cmd/bat

* revert args

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-10-07 15:25:49 -03:00
Lucas Fernandes Nogueira
3d6868d09c feat(cli): UTExportedTypeDeclarations support for file associations (#14128)
* feat(cli): UTExportedTypeDeclarations support for file associations

closes #13314

* update example

* update readme
2025-10-07 13:12:39 -03:00
Lucas Fernandes Nogueira
cc8c0b5317 feat(core): add support to universal app links on macOS (#14031)
* feat(core): add support to universal app links on macOS

follow-up for https://github.com/tauri-apps/tao/pull/1108

* fix ci

* clippy

* ignore empty schemes
2025-10-07 09:27:30 -03:00
renovate[bot]
20e53a4b95 chore(deps): update dependency cross-env to v10.1.0 (#14242)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-07 08:47:31 -03:00
Lucas Fernandes Nogueira
08bda64c25 fix(api): "command not found" error when running addPluginListener (#14132)
* fix(api): "command not found" error when running addPluginListener

the backend expects the command name to be in snake case

we've made this change already for check_permissions and request_permissions, but missed register_listener

* fix check instead

* update bundle.global.js

* code review suggestion

* add note

* adjust change file

* remove unused var

* fmt

* build
2025-10-06 14:55:20 -03:00
Lucas Fernandes Nogueira
28a2f9bc55 fix(cli): ensure Xcode project is up to date with Cargo project name (#14101)
* fix(cli): ensure Xcode project is up to date with Cargo project name

closes #13542

* clippy
2025-10-06 14:06:04 -03:00
Lucas Fernandes Nogueira
ed7c9a4100 feat(core): add config for Info.plist extensions, closes #13667 (#14108)
* feat(core): add config for Info.plist extensions, closes #13667

* add missing tag

* do not lie :)
2025-10-06 14:05:33 -03:00
Lucas Fernandes Nogueira
abf7e8850b fix(cli): mobile init when using pnpm dlx (#14118)
i noticed this when testing #13180 (though the original issue refers to npx, which I could not reproduce yet)
2025-10-06 13:12:00 -03:00
Lucas Fernandes Nogueira
b0012424c5 fix(cli): resolve IP when dev URL host is unspecified, closes #13356 (#14115)
currently the `use_network_address_for_dev_url` function already detects Ipv4Addr::UNSPECIFIED to resolve the local IP address for mobile development when the dev URL host is 0.0.0.0, but we only call it when `--host` is provided or running on a physical device. This change detects the unspecified host early and force the resolution to run even for simulator builds
2025-10-06 13:11:48 -03:00
Fabian-Lars
06d4a4ed6c fix(bundler): set APPIMAGE_EXTRACT_AND_RUN env var as well for linuxdeploy (#14241)
* fix(bundler): set APPIMAGE_EXTRACT_AND_RUN env var as well for linuxdeploy

* Aktualisieren von linuxdeploy-extract.md
2025-10-06 13:11:35 -03:00
renovate[bot]
a99601ee4b chore(deps): update dependency rollup to v4.52.4 (#14256)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-06 19:10:02 +08:00
Vladimir Pankratov
2e089f6acb feat(ios): support async Swift plugin methods (completionHandler:) in PluginManager (#14148)
* Added selector with completionHandler handling

* Added .changes file

* fix change file

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-10-02 09:00:44 -03:00
Lucas Fernandes Nogueira
6bbb530fd5 chore(cli): warn when product name is missing, closes #14034 (#14105) 2025-10-02 08:28:30 -03:00
Lucas Fernandes Nogueira
b06b3bd091 refactor(cli): improve errors (#14126)
* refactor(cli): improve errors

* update change files

* license

* add errorext with fs_context helper

* update linux

* lint

* fmt

* windows

* revert bundler breaking change

* fix ios mod

* ref

* reduce amount of enum variants

* fix macos build

* Fix windows build

* Clippy

* capitalize cargo [skip ci]

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-10-02 06:58:26 -03:00
renovate[bot]
eb60b9966b chore(deps): update dependency rollup to v4.52.3 (#14230)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-10-02 16:34:29 +08:00
kandrelczyk
94cbd40fc7 Add support for adaptive and themed icons on android (#14223)
* add support for adaptive icons

* fix

* small cleanup

* combine android_bg and android_fg when specified

* Update crates/tauri-cli/src/icon.rs

Co-authored-by: Fabian-Lars <github@fabianlars.de>

* add scale option

* properly generated rounded icons

* covector, clippy

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-10-01 09:41:54 -03:00
Lucas Fernandes Nogueira
673867aa0e feat(cli): detect Android env and install SDK and NDK if needed (#14094)
* feat(cli): detect Android env and install SDK and NDK if needed

changes the Android setup to be a bit more automated - looking up ANDROID_HOME and NDK_HOME from common system paths and installing the Android SDK and NDK if needed using the command line tools

* fix windows

* clippy

* lint

* add prmopts and ci check

* also check ANDROID_SDK_ROOT
2025-10-01 09:33:14 -03:00
Rasmus Mecklenburg
4188ffdafc chore(core): allow clippy::used_underscore_binding lint in command macro (#14225) 2025-09-28 13:21:58 +02:00
renovate[bot]
12a6787110 chore(deps): update dependency rollup to v4.52.2 (#14216)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-27 21:54:42 +08:00
renovate[bot]
6cb73194c4 chore(deps): update dependency rollup to v4.52.0 (#14188)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-25 15:41:58 +02:00
FabianLars
d1892b97ce ci(renovate): group windows-rs and webview2 crates 2025-09-25 14:57:50 +02:00
Fabian-Lars
e446926a6a chore: set minimumReleaseAge to 3 days in renovate and pnpm (#14206) 2025-09-25 11:40:25 +02:00
Kirill Gribunin
b0c493a4ea FIX: Fixed GDI object leak when a resizable window is created and then closed on Windows platform (#14209)
Co-authored-by: Kirill Gribunin <kgribunin@contoso.local>
2025-09-24 21:57:05 +03:00
Ryan Seys
d340b8c8b1 fix(macos): check if path is file or dir or neither (#13793) 2025-09-22 10:25:25 +02:00
Shane Cavanaugh
830146d0be chore: remove spam link in readme (#14197) 2025-09-18 18:52:47 +02:00
dependabot[bot]
fa3771b7bc chore(deps-dev): bump vite from 7.0.4 to 7.0.7 (#14172)
* chore(deps-dev): bump vite from 7.0.4 to 7.0.7

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.0.4 to 7.0.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v7.0.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v7.0.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 7.0.7
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* Deduplicate

* pnpm dedupe

* Update vite to 7.1.5

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-09-11 11:14:50 +08:00
renovate[bot]
9efe474e06 chore(deps): update dependency rollup to v4.50.1 (#14169)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-09 11:12:36 +08:00
Tony
69476d8e23 fix(macros): stack overflow in invoke handler (#14170) 2025-09-08 17:38:15 +08:00
Jamie Ridding
f5851ee00d feat: Expose ScrollBarStyle webview option to tauri. (#14089)
* Expose `ScrollBarStyle` webview option to tauri.

This commit exposes the scroll_bar_style option from wry via the tauri
WebviewWindowBuilder API. By itself, the commit does not include changes
to the configuration file or JavaScript APIs: These will be added in a
later commit.

* Fix a compile error on macOS and Linux.

* Add `scroll_bar_style` to WindowConfig.

This commit exposes the `scroll_bar_style` option in tauri.conf.json/
.json5/.toml as `scrollBarStyle` and `scroll-bar-style`.

* Expose `scroll_bar_style` to JavaScript API.

This commit exposes the `scroll_bar_style` in the options object passed
to the JavaScript API `Webview` and `WebviewWindow` constructors.

While testing this, I discovered that on Windows, attempting to create
a webview from the JavaScript API will cause the hosting window to
immediately hang if it attempts to use the same data directory as
another webview without sharing the same environment options. This
commit includes no mitigation for this behaviour, as I will be opening
a separate issue about it at some point in the near future.

* Document WebView2 environment requirements.

This commit adds a message to the documentation for all components of
the `scroll_bar_style` configuration option, telling users that all
webviews that use the same data directory must use the same value for
this option.

* Fix formatting.

* Add change files to .changes directory.

* Remove `tauri-schema-generator` from change file.

* Remove quotes from change tags.

* Add tags to change files.

I did not realise that these were needed, as the pull request that I
used as my reference when building this feature did not have them.

* update conf

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-09-02 07:14:59 -03:00
FabianLars
66cb1dbbef chore(bench): clippy fixes 2025-09-01 20:28:21 +02:00
Kushal Meghani
a58d461eb0 refactor(bench): improve code style (#14062)
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2025-09-01 20:18:20 +02:00
SHIGRAF SALIK
2a06d10066 feat(bundle): add --no-sign flag to skip code signing in bundling pro… (#14052)
* feat(bundle): add --no-sign flag to skip code signing in bundling process

- Introduce a
o_sign option in bundle settings to allow skipping code signing
- Update macOS and Windows bundler implementations to respect the flag
- Wire up CLI option --no-sign to control signing behavior during bundling
- Add necessary config and type changes to propagate the flag throughout bundler

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* ci: added yml for github action testing

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* fix: fixed field 'digest_algorithm' is already declared error

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* ci: updated to test the new features as well

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* ci: fixed yml issue

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* fix: fixed missing parameter issue in android sign.rs

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* chore: apply linting

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* chore: remove redundant files

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* chore: revert indentations

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* fix: added parameters to ios mobile build.rs

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* docs: updated documentation for settigs.rs

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* docs(cli): add documentation for
o_sign flag in build options

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* chore: apply cargo fmt

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* docs: added CHANGES.md

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* refactor(bundler): make
o_sign private and add getter

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* fix: minor error

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* refactor: revert build_benchmark_jsons.rs

Signed-off-by: ShigrafS <shigrafsalik@proton.me>

* impl for macos too

* fix ci

* fix windows build

---------

Signed-off-by: ShigrafS <shigrafsalik@proton.me>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-09-01 13:59:55 -03:00
Fabian-Lars
59089723fc feat(api): add dataDirectory setting config (#14091)
* feat(api): ad dataDirectory setting config

* changefile fmt

* chain, log if dirs::data_local_dir fails

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-09-01 13:56:26 -03:00
Lucas Fernandes Nogueira
1a6627ee7d feat(cli): set default log level when adding the log plugin (#14122)
* feat(cli): set default log level when adding the log plugin

needs https://github.com/tauri-apps/plugins-workspace/pull/2965

ref #14075

* Update crates/tauri-cli/src/add.rs
2025-09-01 13:55:59 -03:00
Lucas Fernandes Nogueira
f6622a3e34 feat(cli): prompt to install iOS runtime if needed, closes #9186 (#14129)
* feat(cli): prompt to install iOS runtime if needed, closes #9186

* ensure runtime is installed

* only when running directly

* use starts_with
2025-09-01 13:55:51 -03:00
github-actions[bot]
80eadb7387 apply version updates (#14100)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-09-01 12:44:16 -03:00
Tony
346a420812 docs: improve resources docs (#14136)
* docs: improve resources docs

* Clippy
2025-09-01 12:15:43 -03:00
renovate[bot]
5239d39149 chore(deps): update dependency rollup to v4.50.0 (#14127)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 17:51:26 +08:00
Lucas Fernandes Nogueira
0b1da30d28 chore(tauri): update documentation for home_dir on iOS (#14121)
* chore(tauri): update documentation for home_dir on iOS

ref #12497

* update
2025-08-30 08:09:38 -03:00
Lucas Fernandes Nogueira
7db7142f9f fix(cli): empty Android emulator name (#14119)
applies https://github.com/tauri-apps/cargo-mobile2/pull/481
2025-08-29 11:45:04 -03:00
Lucas Fernandes Nogueira
a9b342125d fix(cli): iOS simulator dev/build on Apple Intel, closes #13456 (#14114)
applies https://github.com/tauri-apps/cargo-mobile2/pull/479
2025-08-28 18:16:42 -03:00
Lucas Fernandes Nogueira
bcf000c0a8 fix(cli): ios command failing when running with deno, closes #13547 (#14110)
Deno doesn't set an environment variable to help us, so I had to use the exe path to determine whether we're running under deno or not
2025-08-28 18:02:46 -03:00
Lucas Fernandes Nogueira
61b9b681e8 feat(cli): retain all RUST_* env vars on mobile commands (#14111)
useful to propagate RUST_BACKTRACE to the IDE commands
2025-08-28 18:02:22 -03:00
Lucas Fernandes Nogueira
c37a298331 fix(cli): set package type for Deno (#14112)
without the type Deno assumes that the package is a ESM so it cannot use `require`
we should probably update our minimum Node.js version and use ESM instead, but I want to ship this fix first
2025-08-28 18:02:09 -03:00
Lucas Nogueira
b8b866fcc7 fix(examples): update tauri-plugin-log
fixes iOS deadlock
2025-08-28 15:09:36 -03:00
Lucas Fernandes Nogueira
956b4fd6ff fix(cli): export method on Xcode < 15.4, closes #13818 (#14106)
see https://github.com/fastlane/fastlane/issues/22028 for additional context
2025-08-28 12:37:26 -03:00
Lucas Fernandes Nogueira
07e134f70e feat(core): enhance error message for dev server request, closes #13816 (#14107) 2025-08-28 12:36:49 -03:00
Lucas Fernandes Nogueira
f70b28529d feat(cli): ensure mobile Rust targets are installed (#14093)
currently we only install Rust targets for mobile when initializing the projects.

Users can commit the Android/iOS projects, so running the init command is not a requirement to develop mobile apps. This change ensures the Rust targets are installed before trying to compile them.
2025-08-27 18:05:24 -03:00
Fabian-Lars
c23bec62d6 fix: don't set macos deployment target in dev (#14083)
* fix: don't set macos deployment target in dev.

* doc comment

* Update .changes/skip-deployment-target-in-dev.md

* Apply suggestions from code review

---------

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2025-08-27 17:07:07 -03:00
renovate[bot]
9a35a616f5 chore(deps): update dependency rollup to v4.49.0 (#14098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-27 23:27:02 +08:00
Fabian-Lars
755eb33d1c docs: use get_webview_window in example (#14082) 2025-08-25 16:19:44 +02:00
Lucas Nogueira
df61fac2b5 fix(ci): bump tauri-cli to 2.8.3 to match @tauri-apps/cli 2025-08-25 10:32:46 -03:00
github-actions[bot]
16348ac2bd apply version updates (#14081)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 10:30:08 -03:00
Lucas Fernandes Nogueira
03e7c11932 fix(tauri-runtime-wry): ignore about:blank initial URL (#14080)
* fix(tauri-runtime-wry): ignore about:blank initial URL

fixes a macOS warning when a navigation handler is registered and you choose to create a new window on the on_new_window hook - in this case we shouldn't perform the initial navigation since the URL is provided by the webview configuration from the hook

* change tag

* bump min wry
2025-08-25 10:03:35 -03:00
github-actions[bot]
e81635aa3d apply version updates (#14079)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 14:25:58 +02:00
Fabian-Lars
0ac89d3b6c chore(deps): Update cargo-mobile2 for ios 18.6 sim support (#14078) 2025-08-25 14:04:43 +02:00
renovate[bot]
4791d09a0a chore(deps): update dependency rollup to v4.48.1 (#14077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 12:32:49 +02:00
renovate[bot]
bc829ee24d chore(deps): update dependency rollup to v4.48.0 (#14053)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 12:55:18 +08:00
renovate[bot]
11800a0071 chore(deps): update rust crate jsonschema to 0.33 (#14074)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 09:25:53 +08:00
github-actions[bot]
662b39adb3 apply version updates (#14070)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-24 11:38:07 -03:00
Tony
2aaa801c35 Improve documentation of app > windows (#14058) 2025-08-24 10:55:44 -03:00
Fabian-Lars
5349984064 fix: set webview2 path before initializing runtime (#14054)
* fix: set webview2 path before initializing runtime

* wrong current_exe function
2025-08-24 08:18:14 -03:00
Lucas Nogueira
5f535b4150 fix(bench): lint warnings 2025-08-24 08:17:25 -03:00
Tony
f3df96fb38 fix(windows): binary patching 32 bit updater type (#14065)
* fix(windows): binary patching 32 bit updater type

* Use `get` instead of size check and then assert
2025-08-24 08:16:12 -03:00
Lucas Nogueira
c0d3f9d47e chore(bench): fix build and clippy 2025-08-24 07:14:53 -03:00
Kushal Meghani
d54f3b95a6 refactor(bench): improve utils.rs safety, error handling, and docs (#14057)
- Replace `unwrap` and `expect` with `anyhow::Result` for safer error handling
- Add contextual error messages using `anyhow::Context`
- Fix `home_path` on Windows by using `USERPROFILE` instead of `HOMEPATH`
- Ensure process helpers (`run_collect`, `run`) return `Result` instead of panicking
- Improve parsing logic in `parse_max_mem` and `parse_strace_output`
- Add documentation comments for all public functions
- Add best-effort cleanup and resilience against malformed input
2025-08-22 08:04:57 -03:00
renovate[bot]
1e7aac355f chore(deps): update dependency rollup to v4.46.4 (#14049)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-21 12:01:01 +08:00
github-actions[bot]
8d869717da apply version updates (#14041)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-19 09:08:54 -03:00
Kushal Meghani
f0172a454a fix(app): correct removeDataStore return type, add docs for getBundle… (#14038)
* fix(app): correct removeDataStore return type, add docs for getBundleType

* add change file

* fmt

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-19 08:02:21 -03:00
Lucas Fernandes Nogueira
5075b67d36 fix(tauri): build without the wry feature flag (#14039)
* fix(tauri): build without the wry feature flag

* change file

* fix windows
2025-08-19 07:42:41 -03:00
Lucas Nogueira
c3252f72f6 fix(tauri): on_related_view should be behind the wry feature flag 2025-08-18 17:23:06 -03:00
github-actions[bot]
b4abb6cae8 Apply Version Updates From Current Changes (#13887)
* apply version updates

* chore: minor bump codegen, build, macros

* fix audit

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-18 15:50:13 -03:00
Will
1a3d1a024e fix(ios): Tauri iOS build with binary XCFramework dependencies (#13995)
* Fix Tauri iOS build not having a PATH variable to access unzip to extract binaryTargets and also not including Frameworks when linking

* Add covector change

* fmt

* Update crates/tauri-utils/src/build.rs

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>

---------

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2025-08-18 07:35:49 -03:00
renovate[bot]
37154ebdcd chore(deps): update dependency rollup to v4.46.3 (#14027)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-18 14:34:25 +08:00
Tony
380656874e Remove AsRef<Window> on WebviewWindow (#14026) 2025-08-18 13:45:43 +08:00
Lucas Fernandes Nogueira
bc4afe7dd4 feat(cli): check plugin versions for incompatibilities (#13993)
* feat(cli): check plugin versions for incompatibilities

check core plugin versions for incompatibilities between Cargo and NPM releases

a plugin NPM/cargo version is considered "incompatible" if their major or minor versions are not equal

on dev we show an warning
on build we error out (with a `--ignore-incompatible-plugins` flag to prevent that)

this is an idea from @oscartbeaumont
we've seen several plugin changes that require updates for both the cargo and the NPM releases of a plugin, and if they are not in sync, the functionality does not work
e.g. https://github.com/tauri-apps/plugins-workspace/pull/2573 where the change actually breaks the app updater if you miss the NPM update

* Use list to get multiple package versions at once

* Fix for older rust versions

* Clippy

* Support yarn classic

* Support yarn berry

* Use `.cmd` only for `npm`, `yarn`, `pnpm`

* Use yarn list without --pattern

* rename

* Extract function `check_incompatible_packages`

* Check `tauri` <-> `@tauri-apps/api`

* incompatible -> mismatched

* run build check in parallel

* rename struct

* Switch back to use sync check and add todo

* Extract to function `cargo_manifest_and_lock`

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-08-17 12:24:40 -03:00
Akshanabha Chakraborty
7c2eb31c83 feat: add PluginHandle::run_mobile_plugin_async (#13895)
* add async

* chore: fmt

* feat: add run_mobile_plugin_async

* changes

* chore: fix misplaced `}`

* chore: fix minor pattern matching error

* fix: copy the response handling directly from run_mobile_plugin

* fix android build

* Fix clippy lint

* lint

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-17 12:14:16 -03:00
Tony
737364b8d3 fix: a few regressions from previous PRs (#14020)
* fix: a few regressions from previous PRs

* rename with_window_features to window_features

* Clippy

* clippy

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-17 12:03:31 -03:00
Robert
68874c68c5 feat(core): webview window focusable property, closes #11130 (#13564)
* Adds the ability to set the focused property from tauri.conf.json -- windows

* add set_focusable, pin tao

* fmt

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-17 11:50:17 -03:00
Sean Wang
dfadcb764b feat: add WebView::set_cookie and WebView::delete_cookie (#13661)
* chore: patch wry

* feat: added `Webview::set_cookie` and `Webview::delete_cookie`

* chore: changes-files

* fmt

* owned cookie, re-export crate

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-16 23:41:16 -03:00
Sean Wang
22d6bcacbb feat(tauri): impl App::set_device_event_filter for AppHandle also (#14008)
* feat(tauri): impl `App::set_device_event_filter` for `AppHandle` also

* Update .changes/impl-set_device_event_filter-for-apphandle.md

* Update .changes/impl-set_device_event_filter-for-apphandle.md

---------

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2025-08-16 15:30:46 -03:00
Tony
b21d86a8a3 fix(cli): permission add could add duplicated (#13981) 2025-08-16 14:51:45 -03:00
THELOSTSOUL
33d0b3f0c1 feat: add WebviewBuilder::on_new_window and WebviewBuilder::on_document_title_changed (#13876)
* add "new window" and "document title changed" webview handler

* take document title changed handler

* update example, add missing api, change files

* allow creating tauri window for the window.open call

* set size and position, fix linux, example

* enhance document title change

* fix windows deadlock

* wry 0.53

* update wry

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-16 14:49:01 -03:00
Petr
f1232671ab feat: expose internal TrayIcon (#13959) 2025-08-17 00:23:23 +08:00
Lucas Fernandes Nogueira
0c402bfb6b feat(cli): increase iOS deployment target to 14.0 (#13997)
* feat(cli): increase iOS deployment target to 14.0

closes https://github.com/tauri-apps/plugins-workspace/issues/1876

ref https://github.com/tauri-apps/tauri-docs/pull/3455

* fix tests
2025-08-16 10:32:26 -03:00
Lucas Fernandes Nogueira
d6d5f37077 feat: add --root-certificate-path option for mobile dev (#13358)
* feat: add `--root-certificate-path` option for mobile dev

lets you use a HTTPS development server

example usage:
```
cargo install tauri-cli --git https://github.com/tauri-apps/tauri --branch feat/mobile-dev-cert
cargo tauri android dev --open --root-certificate-path "/Users/lucas/Library/Application Support/mkcert/rootCA.pem" --features tauri/rustls-tls
```

* Apply suggestions from code review

Co-authored-by: Fabian-Lars <github@fabianlars.de>

---------

Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-08-16 09:13:10 -03:00
Sean Wang
7261a14368 feat: impl AsRef<Window> and on_webview_event for WebviewWindow (#14012) 2025-08-16 11:01:53 +08:00
Sean Wang
0e6b5cbe5f feat(tauri): re-export PixelUnit, PhysicalUnit, LogicalUnit (#14009)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-08-15 15:24:34 +02:00
Sean Wang
a3dc42477a feat(tauri): export TitleBarStyle for all platforms (#14013) 2025-08-15 15:13:05 +02:00
Sean Wang
21ebc6e820 feat(tauri): remove 'static lifetime bound from AppHandle::remove_plugin (#14007) 2025-08-15 15:28:39 +08:00
Lucas Fernandes Nogueira
2d5f5a9230 chore: update tests (#13998) 2025-08-13 13:34:01 -03:00
Fabian-Lars
4475e93e13 feat(bundler/cli): Add feature flag to use system certificates (#13824) 2025-08-12 13:30:23 +02:00
Naman Khandelwal
5110a762e9 feat(window): add macOS window::set_simple_fullscreen (closes #13670) (#13830)
* add implementation of set_simple_fullscreen

* add simple fullscreen API for macos

* register desktop command

* format

* fix errors

* chore: format

* change implementation

* add api

* fix tests

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-12 08:03:29 -03:00
Fabian-Lars
a9ec12843a feat: add option to not wait on notarization to finish (#13521)
* feat: add option to not wait on notarization to finish

* cli arg istead of config

* changefile

* fix serde

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-08-11 14:29:41 -03:00
Tony
f0dcf9637c fix(cli): add single-instance autostart init code (#13980) 2025-08-11 19:40:18 +08:00
Tony
196ace3c04 fix: return error on window creation failed (#13970)
* fix: return error on window creation failed

* Add todo about adding error inside `CreateWindow`
2025-08-09 22:43:18 +08:00
Aly Cerruti
82e264552e fix(windows): fix double free (STATUS_HEAP_CORRUPTION) of resizing handler's userdata (#13968)
* fix: double free of resizing handler's userdata on Windows

Using WM_NCDESTROY instead of WM_DESTROY is more correct for freeing userdata, as windows can receive multiple WM_DESTROY events if they're parented.

* chore: add change entry for resizing handler double-free fix
2025-08-09 12:03:21 +08:00
Tony
c134a769ea chore: fix some warnings on new rust version (#13965)
* chore: fix some warnings on new rust version

* No main

* allow dead code on specta Channel
2025-08-09 08:19:03 +08:00
Fabian-Lars
390cb9c36a fix(cli): reduce log level for goblin and handlebars (#13953) 2025-08-07 14:40:41 +02:00
Jaken Herman
9300b59f65 feat: Added fips_compliant field to WixConfig (#13787)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-08-05 20:35:25 +02:00
Jadon Jesse
e1d7be8e57 fix(example): runtime crash when counter less than 0 (#13955) 2025-08-05 18:45:49 +02:00
renovate[bot]
90c1c327ac chore(deps): update dependency cross-env to v10 (#13894)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 17:15:22 +02:00
renovate[bot]
83032e273b chore(deps): update rust crate which to v8 (#13711)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-05 14:59:02 +02:00
Tony
a8f1569b04 fix(windows): bundler should not sign non-binaries (#13921)
* fix(windows): bundler should not sign non-binaries

* Fix non Windows
2025-08-05 11:06:43 +08:00
Tony
0ea08e901e fix(example): unminimize window on tray icon click (#13949) 2025-08-05 10:07:16 +08:00
Fabian-Lars
887b8da684 fix(bundler): improve log format of sign command stdout (#13947) 2025-08-04 15:38:56 +02:00
Tony
7d21e3b2fa docs: how security > capabilities works (#13946)
* docs: how `security > capabilities` works

* Add how to use it

* Apply suggestions

* Relative to `Cargo.toml`

* Remove the relative base wording
2025-08-04 18:13:02 +08:00
Sam Lidder
4d270a96a8 fix(windows): patch_binary causing codesigning verification failure (#13943)
* fix(windows): `patch_binary` causing codesigning verification failure

* `cargo fmt`

* add change file

* Update .changes/fix-binary-patching-codesign-verification-failure.md

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-08-04 16:58:04 +08:00
renovate[bot]
bcc7a82a3a chore(deps): update rust crate notify-debouncer-full to 0.6 (#13945)
* chore(deps): update rust crate notify-debouncer-full to 0.6

* Bump jsonschema
2025-08-04 10:39:34 +08:00
Fabian-Lars
8b465a12ba fix(bundler/linux): pull latest appimage plugin (#13913) 2025-07-31 20:06:57 +02:00
Tony
ee68c918a1 chore(deps): unpin serialize-to-javascript (#13932) 2025-07-31 17:52:51 +08:00
renovate[bot]
d7075b66bd chore(deps): update rust crate toml to 0.9 (#13784)
* chore(deps): update rust crate toml to 0.9

* Bump toml_edit and cargo_toml

* Update tauri-winres

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-31 17:14:09 +08:00
xeodus
bbcea1f5e8 fix(cli): improve error messages regarding cargo metadata command (#13918) 2025-07-30 11:23:54 +02:00
Pavel Kuzmin
5ba1c3faa4 feat(menu): add icon support for Submenu in Rust and JS/TS APIs (#13722)
* feat(menu): add icon and nativeIcon support for Submenu in tauri and @tauri-apps/api

* Merge branch 'dev' into dev

* Update muda

* feat(menu): add set_icon and set_native_icon methods to set submenu icons

* feat(menu): unify icon handling by introducing MenuIcon type

* chore: sync bundle.global.js

* Make setIcon actually work

* Regenerate `bundle.global.js`

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-30 13:48:55 +08:00
renovate[bot]
e27427f795 chore(deps): update dependency rollup to v4.46.2 (#13914)
* chore(deps): update dependency rollup to v4.46.2

* Bump @eslint/plugin-kit

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-30 09:39:22 +08:00
renovate[bot]
a32a4ce3be chore(deps): update rust crate jsonschema to 0.32 (#13915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-30 09:26:04 +08:00
Andrew Voynov
bc6b125b24 fix(bundler): replace empty RPM release value with 1 (#13909)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-07-29 22:20:25 +02:00
Takeaki Kobayashi
9c938be452 fix(cli): properly migrate svelte to v5 in the plugin example template (#13912)
* fix: update vite.config.ts to support Svelte 4 in example app

* Migrate the code to svelte 5 instead

* Add change file

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-29 17:47:17 +08:00
renovate[bot]
5c8182860c chore(deps): update rust crate jsonschema to 0.31 (#13903)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 23:29:31 +08:00
renovate[bot]
1d31e4647c chore(deps): update dependency rollup to v4.46.1 (#13904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-28 15:19:30 +08:00
renovate[bot]
517e7b60e1 chore(deps): update dependency rollup to v4.46.0 (#13897)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-27 20:38:49 +08:00
Robin van Boven
72b4226ee9 feat: reduce Debug format size for binary buffers (#13809)
* feat: reduce Image debug output

For example now:
`Image { rgba: Cow::Borrowed([u8; 4096]), width: 32, height: 32 }) }`

* feat: reduce EmbeddedAssets debug size

For example now:
```
EmbeddedAssets {
    assets: {
        "/index.html": [u8; 1835],
        "/index.js": [u8; 212],
    },
    global_hashes: [
        Script(
            "'sha256-EOd6N98xxmK5s7VvxV7W2w7YG+dmP52MqNiZUq1NLeE='",
        ),
        Style(
            "'sha256-YEercZJImS+vUX2bz7vkQ0aA4rtBIPLuCEWz+yraQ/g='",
        ),
    ],
    html_hashes: {
        "/index.html": [
            Script(
                "'sha256-3g8CfFrjFLGpwD2o+hwMt+lh/hsHbQ3XY+EPJ35fFKk='",
            ),
            Script(
                "'sha256-EOd6N98xxmK5s7VvxV7W2w7YG+dmP52MqNiZUq1NLeE='",
            ),
        ],
    },
}
```

* feat: reduce `app_icon` debug size

* chore: changelog

* chore: include tauri-utils in changelog

* doc: comment had extra closing brackets [skip ci]
2025-07-25 07:55:00 -03:00
Lucas Fernandes Nogueira
d6d941c3a7 chore(cli): update plugin template (#13882) 2025-07-25 07:50:41 -03:00
Lucas Fernandes Nogueira
a0113a8c64 feat(tauri-plugin): add build::mobile::update_info_plist (#13888)
* feat(tauri-plugin): add build::mobile::update_info_plist

needed for https://github.com/tauri-apps/plugins-workspace/pull/2870

* Update .changes/update-info-plist.md
2025-07-25 07:50:24 -03:00
Jack Lavigne
91508c0b8d feat: add config option for custom watch folders (#13881)
* feat: add config option for custom watch folders

* fix: cargo clippy changes

* chore: remove file

* fix: ios dev

* docs: clarify absolute allowed

* refactor: rename variable

* fix: review suggestions

* fix: resolve paths

* fixL use canonicalize

* chore: add changefile

* chore: add error if cant canonicalize

* reformat changelog

* Update .changes/additional-watch-folders.md

* Update crates/tauri-cli/src/interface/rust.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* Revert "Update .changes/additional-watch-folders.md"

This reverts commit 98186b1a89.

* Also bump `@tauri-apps/cli`

* Apparently I'm so used to a higher rust version

* Revert "Apparently I'm so used to a higher rust version"

This reverts commit ea1d89e2d3.

* Need to check for existence for abs paths as well

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-24 22:21:00 +08:00
github-actions[bot]
fd63f229d5 apply version updates (#13871)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-22 12:32:14 +08:00
Krishna Chaitanya
af95fb6014 fix: sign main binary after patching with bundle info (fix #13868) (#13870)
* fixed #13868

* add main binary singing only on windows

* updated readme message
2025-07-22 11:32:49 +08:00
FabianLars
65bb24b9ae fix(cli): fix metadata version 2025-07-21 10:10:12 +02:00
FabianLars
332ec355a1 fix(cli): add default triplets to napi targets config 2025-07-21 09:59:32 +02:00
renovate[bot]
2c46b1873e chore(deps): update dependency eslint-config-prettier to v10.1.8 (#13855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-21 10:23:19 +08:00
FabianLars
96439c2c42 chore(deps): Update @eslint/plugin-kit to fix pnpm audit 2025-07-21 00:37:41 +02:00
github-actions[bot]
ab97f36b64 apply version updates (#13751)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-07-21 00:09:52 +02:00
Tony
6a4451bcd9 fix(windows): isolation pattern create iframe loop (#13849) 2025-07-20 23:33:14 +02:00
Tony
56277e4722 chore(deps): update napi to v3 (#13852) 2025-07-20 23:03:51 +02:00
Fabian-Lars
7a6fd5b75d fix(bundler): Move AppRun to mirror (#13863) 2025-07-20 23:00:19 +02:00
Sean Wang
7f3c989111 feat(tauri): add plugin_boxed methods (#13837)
* feat(tauri): add `plugin_dyn` methods

* refactor: rename `plugin_dyn` to `plugin_boxed`
2025-07-18 11:48:10 +08:00
Tony
bda8304107 fix(cli): error out when migrating from v2 alpha (#13833) 2025-07-16 22:06:17 +08:00
Fabian-Lars
fb9d9c7fd1 chore(readme): Update discord invite (#13836) 2025-07-16 11:02:23 +02:00
renovate[bot]
8263b412c6 chore(deps): update dependency rollup to v4.45.1 (#13831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-15 23:10:12 +08:00
Siddharth
3025d90951 feat: expose some window props from runtime-wry (#13822)
* test: make some of window id + stores public

* test: make window wrapper label pub

* feat: make label accessible for windowwrapper

* chore: adds wry runtime changefile

* chore: avoid forced clone on label

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-07-15 14:32:19 +08:00
Joseph Brooksbank
96391467e9 feat(mock): add mock for listen and emit (#13783)
* feat(mock): add mock for listen and emit
* feat(mock): add mock for listen and emit

* feat(mock): add mock for listen and emit

* Add change file

* correctly clear unregisterListener

* format with prettier

* build project

* opt-in to mocking events

* Use a minor bump
2025-07-15 09:30:01 +08:00
kandrelczyk
c0a654b863 failed binary patch warning (#13825)
* failed binary patch warning

* Update crates/tauri-bundler/src/bundle.rs

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-07-14 22:24:11 +08:00
Lucas Raposeiras
b821796add docs: add missing trafficLightPosition to WindowOptions (fix #13790) (#13810)
* docs: add missing `trafficLightPosition` to `WindowOptions`

* Change file and tweak docs

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-13 20:58:39 +08:00
Mohammad Hossein Bagheri
33d079392a feat(cli): allow runner configuration to be an object with cwd and args (#13811)
* Update config.schema.json

* Add RunnerConfig for customizable build runner

Replaces runner String with RunnerConfig in CLI and config, allowing advanced runner configuration via string or object with cmd, cwd, and args. Updates schema and usage to support new format, and adds tests for serialization, deserialization, and API. Enables more flexible build and run command customization.

* Create runner-object-config.md

* Remove unused RunnerConfig import in tests

Cleaned up the test module in config.rs by removing the unused RunnerConfig import from two test functions.

* Fix tests failing

Updates related tests in tauri-utils to improve readability and maintain consistency. Minor import reordering in tauri-cli for clarity.

* Move RunnerConfig enum and impls above BuildConfig

Relocated the RunnerConfig enum and its associated implementations to appear before the BuildConfig definition. This improves code organization and logical grouping of configuration-related types.
2025-07-13 19:28:09 +08:00
Tony
7bc77a038a feat: allow all frame init scripts in plugin (#13609)
* feat: allow all frame init scripts in plugin

* Add change files

* Update crates/tauri/src/plugin.rs

Co-authored-by: Fabian-Lars <github@fabianlars.de>

* Default impl initialization_script_2 from 1

* Update crates/tauri/src/plugin.rs

Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>

---------

Co-authored-by: Fabian-Lars <github@fabianlars.de>
Co-authored-by: Lucas Fernandes Nogueira <lucas@tauri.app>
2025-07-13 14:12:39 +08:00
kandrelczyk
371ee34383 make static bundle type var mutable (#13812)
* make static bundle type var mutable

* remove unsafe from no_mangle and link_section
2025-07-12 18:48:13 +08:00
renovate[bot]
22cd1e2846 chore(deps): update dependency rollup to v4.45.0 (#13813)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-12 17:55:16 +08:00
M
1c5df96fe8 fix(protocol): proxy body in mobile dev (#13773)
* proxy body in mobile dev

* add change file, use std::mem::take

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-07-11 07:22:46 -03:00
kandrelczyk
4f96ed41ca add #[used] to __TAURI_BUNDLE_TYPE so that it's not stripped in release builds (#13808) 2025-07-11 15:52:37 +08:00
renovate[bot]
24eb2b1cd3 chore(deps): update dependency @sveltejs/vite-plugin-svelte to v6 (#13803)
* chore(deps): update dependency @sveltejs/vite-plugin-svelte to v6

* Bump vite

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-07-11 13:22:46 +08:00
Lucas Fernandes Nogueira
0f248b111f fix(cli): install iOS dependencies when needed (#13799)
currently deps are only installed on init, which might not be executed on someone's machine if the xcode project is commited to the repo. we need to ensure dependencies are installed before running them

applies https://github.com/tauri-apps/cargo-mobile2/pull/468
2025-07-10 10:17:23 -03:00
Lucas Fernandes Nogueira
4ba871c5d2 fix(core): loading remote URLs on mobile, closes #13461 (#13782) 2025-07-09 17:08:38 -03:00
Catalin Andrei Cacuci
f94af90359 fix(nsis): per-machine installer not requesting elevation when run by non-admin users (#13786) 2025-07-09 13:42:07 +02:00
Tony
cfc5bb8196 feat(android): edge to edge in init template (#13780)
* feat(android): edge to edge in init template

* Add androidx.activity:activity-ktx

* androidx.webkit:webkit:1.14.0
2025-07-09 10:05:27 +08:00
Lucas Fernandes Nogueira
916aeaa486 fix(cli): android commands reading iOS config closes #13765 (#13781) 2025-07-08 08:49:47 -03:00
Jeong Min Oh
12e3590613 Fix(package.json) build script, Fix(android) build template (#13759)
* Update compileSdk targetSdk, Fix build cmd

* Add changelog

* Update AGP version

* Update package.json

* Update AGP

* Update SDK

* Update

* Update .changes/update-android-sdk.md

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>

* Use changes tag in change file

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
2025-07-08 17:00:26 +08:00
kandrelczyk
232265c70e feat: bundle type detection at runtime via binary patching (#13209)
* patch binary with bundle type info

* only patch if the updater is included

* fix linux warnings

* patch binary when updaer is configured

* patch binary with bundle type info

only patch if the updater is included

fix linux warnings

patch binary when updaer is configured

* fix formatting

* fix license header

* fix taplo error

* move __TAURI_BUNDLE_TYPE to utils

* export get_current_bundle_type

* macos fix

* cleanup, add api

* update change file

* fix windows

* fmt, fix rust version support

* fix macos

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
2025-07-07 12:08:00 -03:00
renovate[bot]
02440b875c chore(deps): update dependency rollup to v4.44.2 (#13766)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-07-06 20:39:00 +08:00
Catalin Andrei Cacuci
f2dbe73097 fix(nsis): wrong required files path for nsis_tauri_utils.dll (#13772)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-07-05 23:42:25 +02:00
Tony
152d971bcd fix(mock): expose callback functions (#13744)
* fix(mock): expose callback functions

* Add change file

* Bumped the wrong package

* Fix end quote in comment
2025-07-02 22:23:23 +08:00
Jeong Min Oh
acd7574284 fix(cli/ios): fix CFBundleVersion type (#13743)
Co-authored-by: devfive <devfive@devfiveui-MacStudio.local>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-07-02 16:01:59 +02:00
renovate[bot]
e296e4bc38 chore(deps): update dependency @rollup/plugin-typescript to v12.1.4 (#13729)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-06-30 14:07:17 +02:00
AprilNEA
11b4a03881 fix(cli): remove unnecessary files in npm package, closes #12139 (#13735)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-06-30 14:02:03 +02:00
353 changed files with 21486 additions and 8104 deletions

View File

@@ -6,4 +6,6 @@ ignore = [
"RUSTSEC-2020-0095",
# proc-macro-error is unmaintained
"RUSTSEC-2024-0370",
# time crate can't be updated in the repo because of MSRV, users are unaffected
"RUSTSEC-2026-0009",
]

View File

@@ -0,0 +1,6 @@
---
'tauri': 'minor:feat'
'@tauri-apps/api': 'minor:feat'
---
Add Bring All to Front predefined menu item type

5
.changes/base64.md Normal file
View File

@@ -0,0 +1,5 @@
---
"tauri-macos-sign": patch:enhance
---
Do not rely on system base64 CLI to decode certificates.

View File

@@ -0,0 +1,6 @@
---
"tauri-build": patch:enhance
"tauri": patch:enhance
---
Simplify async-sync code boundaries, no externally visible changes

View File

@@ -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,

View File

@@ -0,0 +1,5 @@
---
"tauri": minor:feat
---
Add `data-tauri-drag-region="deep"` so clicks on non-clickable children will drag as well. Can still opt out of drag on some regions using `data-tauri-drag-region="false"`

View File

@@ -0,0 +1,7 @@
---
'tauri': 'minor:feat'
'tauri-runtime': 'minor:feat'
'tauri-runtime-wry': 'minor:feat'
---
Add `eval_with_callback` to the Tauri webview APIs and runtime dispatch layers.

View File

@@ -0,0 +1,13 @@
---
"tauri-bundler": minor:feat
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
"tauri-utils": minor:feat
---
Added uninstaller icon and uninstaller header image support for NSIS installer.
Notes:
- For `tauri-bundler` lib users, the `NsisSettings` now has 2 new fields `uninstaller_icon` and `uninstaller_header_image` which can be a breaking change
- When bundling with NSIS, users can add `uninstallerIcon` and `uninstallerHeaderImage` under `bundle > windows > nsis` to configure them.

View File

@@ -0,0 +1,7 @@
---
"tauri-bundler": patch:bug
"tauri-cli": patch:bug
"@tauri-apps/cli": patch:bug
---
Fix `build --bundles` to allow `nsis` arg in linux+macOS

View File

@@ -0,0 +1,6 @@
---
"tauri-cli": patch:bug
"@tauri-apps/cli": patch:bug
---
Fix iOS build failure when `Metal Toolchain` is installed by using explicit `$(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain` path instead of `$(TOOLCHAIN_DIR)` for Swift library search paths.

View File

@@ -0,0 +1,5 @@
---
"tauri-bundler": patch:bug
---
Correct GitHub Release URL path for Linux i686 tooling.

View File

@@ -0,0 +1,5 @@
---
"tauri-bundler": minor:feat
---
Added support to Liquid Glass icons.

View File

@@ -0,0 +1,8 @@
---
"tauri": minor:feat
"tauri-runtime-wry": minor:feat
"tauri-runtime": minor:feat
"tauri-utils": minor:feat
---
Support creating multiple windows on Android (activity embedding) and iOS (scenes).

View File

@@ -0,0 +1,6 @@
---
"tauri": minor:changes
"tauri-runtime-wry": minor:changes
---
The new window handler passed to `on_new_window` no longer requires `Sync`, and runs on main thread on Windows, aligning with other platforms

View File

@@ -0,0 +1,6 @@
---
"tauri-cli": patch:enhance
"@tauri-apps/cli": patch:enhance
---
Show the context before prompting for updater signing key password

View File

@@ -0,0 +1,5 @@
---
"tauri": patch:enhance
---
Implement retrieving inner PathBuf from SafePathBuf to ease using APIs that require an owned PathBuf

View File

@@ -0,0 +1,6 @@
---
"tauri-utils": minor:deps
---
Add new `html-manipulation-2` and `build-2` feature flags that use `dom_query` instead of `kuchikiki` for HTML parsing / manipulation.
This allows downstream users to remove `kuchikiki` and its dependencies from their dependency tree.

5
.changes/tauri-dbus.md Normal file
View File

@@ -0,0 +1,5 @@
---
tauri: minor:feat
---
Added `dbus` feature flag (enabled by default) which is required for theme detection on Linux.

5
.changes/toml-ver.md Normal file
View File

@@ -0,0 +1,5 @@
---
tauri-utils: patch:deps
---
Changed `toml` crate version from `0.9` to `">=0.9, <=1"`

View File

@@ -0,0 +1,12 @@
---
"tauri-bundler": minor:feat
"tauri-cli": minor:feat
"@tauri-apps/cli": minor:feat
---
Added support for `minimumWebview2Version` option support for the MSI (Wix) installer, the old `bundle > windows > nsis > minimumWebview2Version` is now deprecated in favor of `bundle > windows > minimumWebview2Version`
Notes:
- For anyone relying on the `WVRTINSTALLED` `Property` tag in `main.wxs`, it is now renamed to `INSTALLED_WEBVIEW2_VERSION`
- For `tauri-bundler` lib users, the `WindowsSettings` now has a new field `minimum_webview2_version` which can be a breaking change

View File

@@ -5,6 +5,7 @@ Hi! We, the maintainers, are really excited that you are interested in contribut
- [Issue Reporting Guidelines](#issue-reporting-guidelines)
- [Pull Request Guidelines](#pull-request-guidelines)
- [Development Guide](#development-guide)
- [AI Tool Policy](#ai-tool-policy)
## Issue Reporting Guidelines
@@ -33,11 +34,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 +97,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
@@ -106,6 +105,16 @@ $ RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features --open
The JS API provides bindings between the developer's JS in the Webview and the built-in Tauri APIs, written in Rust. Its code is located in [/packages/api](https://github.com/tauri-apps/tauri/tree/dev/packages/api).
After making changes to the code, run `pnpm build` to build it. To test your changes, we recommend using the API example app, located in [/examples/api](https://github.com/tauri-apps/tauri/tree/dev/examples/api). It will automatically use your local copy of the JS API and provides a helpful UI to test the various commands.
## AI Tool Policy
It takes a lot of time to review a Pull Request while it's very easy to make a nonsensical but plausible looking one using AI tools.
It is unfair for other contributors and the reviewers to spend much of the time dealing with this, hence these rules:
1. Review and test all LLM-generated content before submitting, you're the one responsible for it, not the AI.
2. Don't use AI to respond to review comments (except for translations).
We will close the Pull Request with a `ai-slop` tag if you failed to do so.
## Financial Contribution
Tauri is an MIT-licensed open source project. Its ongoing development can be supported via [GitHub Sponsors](https://github.com/sponsors/tauri-apps) or [Open Collective](https://opencollective.com/tauri). We prefer GitHub Sponsors as donations made are doubled through the matching fund program.

View File

@@ -8,7 +8,16 @@ on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
pull_request:
paths:
- '.github/workflows/audit.yml'
- '**/Cargo.lock'
- '**/Cargo.toml'
- '**/package.json'
- '**/pnpm-lock.yaml'
push:
branches:
- dev
paths:
- '.github/workflows/audit.yml'
- '**/Cargo.lock'
@@ -26,7 +35,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: rust audit
uses: rustsec/audit-check@v1
uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -61,7 +61,7 @@ jobs:
actions: write # required for workflow_dispatch
contents: write # required to create new releases
pull-requests: write # required to open version update pr
id-token: write # pnpm provenance
id-token: write # pnpm provenance / oidc token
outputs:
change: ${{ steps.covector.outputs.change }}
commandRan: ${{ steps.covector.outputs.commandRan }}
@@ -74,11 +74,9 @@ jobs:
with:
fetch-depth: 0
- run: npm i -g --force corepack
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
node-version: 24
- name: cargo login
run: cargo login ${{ secrets.ORG_CRATES_IO_TOKEN }}
@@ -96,7 +94,6 @@ jobs:
uses: jbolda/covector/packages/action@covector-v0
id: covector
env:
NODE_AUTH_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
CARGO_AUDIT_OPTIONS: ${{ secrets.CARGO_AUDIT_OPTIONS }}
NPM_CONFIG_PROVENANCE: true
with:

View File

@@ -20,6 +20,10 @@ defaults:
run:
working-directory: packages/cli/
permissions:
contents: write # update release
id-token: write # oidc token
jobs:
build:
strategy:
@@ -116,7 +120,7 @@ jobs:
if: ${{ !matrix.settings.docker }}
with:
targets: ${{ matrix.settings.target }}
- uses: Swatinem/rust-cache@v1
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.settings.target }}
if: ${{ matrix.settings.docker }}
@@ -366,16 +370,13 @@ jobs:
- test-linux-x64-gnu-binding
- test-linux-x64-musl-binding
#- test-linux-arm-bindings
permissions:
contents: write # update release
id-token: write # npm provenance
steps:
- uses: actions/checkout@v4
- run: npm i -g --force corepack
- name: Setup node
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 20
node-version: 24
cache: 'pnpm'
- name: Install dependencies
run: pnpm i --frozen-lockfile --ignore-scripts
@@ -390,10 +391,8 @@ jobs:
shell: bash
- name: Publish
run: |
echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc
npm publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.ORG_NPM_TOKEN }}
NODE_AUTH_TOKEN: ''
RELEASE_ID: ${{ github.event.client_payload.releaseId || inputs.releaseId }}
NPM_CONFIG_PROVENANCE: true

View File

@@ -38,35 +38,62 @@ jobs:
rust_target: aarch64-pc-windows-msvc
ext: '.exe'
args: ''
- os: ubuntu-22.04
rust_target: riscv64gc-unknown-linux-gnu
ext: ''
args: ''
cross: true
steps:
- uses: actions/checkout@v4
- name: 'Setup Rust'
if: ${{ !matrix.config.cross }}
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.config.rust_target }}
- uses: Swatinem/rust-cache@v2
if: ${{ !matrix.config.cross }}
with:
key: ${{ matrix.config.rust_target }}
- name: install Linux dependencies
if: matrix.config.os == 'ubuntu-latest'
if: ${{ !matrix.config.cross && startsWith(matrix.config.os, 'ubuntu') }}
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev
- name: Install cross
if: ${{ matrix.config.cross }}
uses: taiki-e/install-action@v2
with:
tool: cross@0.2.5
- name: Build CLI
if: ${{ !matrix.config.cross }}
run: cargo build --manifest-path ./crates/tauri-cli/Cargo.toml --profile release-size-optimized ${{ matrix.config.args }}
- name: Build CLI (cross)
if: ${{ matrix.config.cross }}
run: cross build --manifest-path ./crates/tauri-cli/Cargo.toml --target ${{ matrix.config.rust_target }} --profile release-size-optimized ${{ matrix.config.args }}
- name: Upload CLI
if: ${{ !matrix.config.cross }}
uses: actions/upload-artifact@v4
with:
name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }}
path: target/release-size-optimized/cargo-tauri${{ matrix.config.ext }}
if-no-files-found: error
- name: Upload CLI (cross)
if: ${{ matrix.config.cross }}
uses: actions/upload-artifact@v4
with:
name: cargo-tauri-${{ matrix.config.rust_target }}${{ matrix.config.ext }}
path: target/${{ matrix.config.rust_target }}/release-size-optimized/cargo-tauri${{ matrix.config.ext }}
if-no-files-found: error
upload:
needs: build
runs-on: ubuntu-latest

View File

@@ -90,15 +90,27 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
prefix-key: v3
key: ${{ matrix.platform.target }}
save-if: ${{ matrix.features.key == 'all' }}
- name: test
- name: test tauri-utils
if: ${{ !matrix.platform.cross }}
run: cargo ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --manifest-path crates/tauri/Cargo.toml
# Using --lib --bins --tests to skip doc tests
run: cargo ${{ matrix.platform.command }} --target ${{ matrix.platform.target }} ${{ matrix.features.args }} --lib --bins --tests --manifest-path crates/tauri-utils/Cargo.toml
- name: test (using cross)
- name: test tauri-utils (using cross)
if: ${{ matrix.platform.cross }}
# Using --lib --bins --tests to skip doc tests
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 }} --lib --bins --tests --manifest-path crates/tauri-utils/Cargo.toml
- name: test tauri
if: ${{ !matrix.platform.cross }}
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 tauri (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

109
.gitignore vendored
View File

@@ -1,54 +1,55 @@
# dependency directories
node_modules/
# Optional npm and yarn cache directory
.npm/
.yarn/
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env
# .vscode workspace settings file
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json
# npm, yarn and bun lock files
package-lock.json
yarn.lock
bun.lockb
# rust compiled folders
target/
# test video for streaming example
streaming_example_test_video.mp4
# examples /gen directory
/examples/**/src-tauri/gen/
/bench/**/src-tauri/gen/
# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# runtime data
pids
*.pid
*.seed
*.pid.lock
# miscellaneous
/.vs
.DS_Store
.Thumbs.db
*.sublime*
.idea
debug.log
TODO.md
# dependency directories
node_modules/
# Optional npm and yarn cache directory
.npm/
.yarn/
# Output of 'npm pack'
*.tgz
# dotenv environment variables file
.env
# .vscode workspace settings file
.vscode/settings.json
.vscode/launch.json
.vscode/tasks.json
# npm, yarn and bun lock files
package-lock.json
yarn.lock
bun.lockb
# rust compiled folders
target/
# test video for streaming example
streaming_example_test_video.mp4
# examples /gen directory
/examples/**/src-tauri/gen/
/bench/**/src-tauri/gen/
# logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# runtime data
pids
*.pid
*.seed
*.pid.lock
# miscellaneous
/.vs
.DS_Store
.Thumbs.db
*.sublime*
.idea
debug.log
TODO.md
.aider*

View File

@@ -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} `
)
}
}

View 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({

View File

@@ -1,16 +0,0 @@
.changes
.devcontainer
.docker
.github
.scripts
.vscode
audits
bench
packages/api
packages/cli
crates/tauri-cli
crates/tauri-bundler
crates/tauri-driver
crates/tauri-macos-sign
crates/tauri-schema-generator
crates/tests

1594
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -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" }

View File

@@ -4,7 +4,7 @@
[![License](https://img.shields.io/badge/License-MIT%20or%20Apache%202-green.svg)](https://opencollective.com/tauri)
[![test core](https://img.shields.io/github/actions/workflow/status/tauri-apps/tauri/test-core.yml?label=test%20core&logo=github)](https://github.com/tauri-apps/tauri/actions/workflows/test-core.yml)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Ftauri-apps%2Ftauri?ref=badge_shield)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.gg/SpmNs4S)
[![Chat Server](https://img.shields.io/badge/chat-discord-7289da.svg)](https://discord.com/invite/tauri)
[![website](https://img.shields.io/badge/website-tauri.app-purple.svg)](https://tauri.app)
[![https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg](https://good-labs.github.io/greater-good-affirmation/assets/images/badge.svg)](https://good-labs.github.io/greater-good-affirmation)
[![support](https://img.shields.io/badge/sponsor-Open%20Collective-blue.svg)](https://opencollective.com/tauri)
@@ -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

View File

@@ -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;

View File

@@ -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(())

View File

@@ -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(())
}

View File

@@ -1,5 +1,79 @@
# Changelog
## \[2.5.6]
### Dependencies
- Upgraded to `tauri-utils@2.8.3`
- Upgraded to `tauri-codegen@2.5.5`
## \[2.5.5]
### Dependencies
- Upgraded to `tauri-codegen@2.5.4`
## \[2.5.4]
### Enhancements
- [`2d28e3143`](https://www.github.com/tauri-apps/tauri/commit/2d28e3143ee3d97d7570ea03877aa00a0d6e47d0) ([#14632](https://www.github.com/tauri-apps/tauri/pull/14632) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Small code refactors for improved code readability. No user facing changes.
### Dependencies
- Upgraded to `tauri-utils@2.8.2`
- Upgraded to `tauri-codegen@2.5.3`
## \[2.5.3]
### Dependencies
- Upgraded to `tauri-utils@2.8.1`
- Upgraded to `tauri-codegen@2.5.2`
## \[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
- Upgraded to `tauri-utils@2.6.0`
- Upgraded to `tauri-codegen@2.3.1`
## \[2.2.1]
### Dependencies

View File

@@ -1,6 +1,6 @@
[package]
name = "tauri-build"
version = "2.3.0"
version = "2.5.6"
description = "build time code to pair with https://crates.io/crates/tauri"
exclude = ["CHANGELOG.md", "/target"]
readme = "README.md"
@@ -22,15 +22,13 @@ 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.0", path = "../tauri-codegen", optional = true }
tauri-utils = { version = "2.5.0", path = "../tauri-utils", features = [
"build",
tauri-codegen = { version = "2.5.5", path = "../tauri-codegen", optional = true }
tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [
"build-2",
"resources",
] }
cargo_toml = "0.22"
@@ -43,7 +41,6 @@ tauri-winres = "0.3"
semver = "1"
dirs = "6"
glob = "0.3"
toml = "0.8"
# Our code requires at least 0.8.21 so don't simplify this to 0.8
schemars = { version = "0.8.21", features = ["preserve_order"] }

View File

@@ -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,
})

View File

@@ -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 {

View File

@@ -57,7 +57,7 @@ fn copy_binaries(
binaries: ResourcePaths,
target_triple: &str,
path: &Path,
package_name: Option<&String>,
package_name: Option<&str>,
) -> Result<()> {
for src in binaries {
let src = src?;
@@ -165,21 +165,21 @@ fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
.with_context(|| format!("Failed to create frameworks output directory at {dest_dir:?}"))?;
for framework in frameworks.iter() {
if framework.ends_with(".framework") {
let src_path = PathBuf::from(framework);
let src_path = Path::new(framework);
let src_name = src_path
.file_name()
.expect("Couldn't get framework filename");
let dest_path = dest_dir.join(src_name);
copy_dir(&src_path, &dest_path)?;
copy_dir(src_path, &dest_path)?;
continue;
} else if framework.ends_with(".dylib") {
let src_path = PathBuf::from(framework);
let src_path = Path::new(framework);
if !src_path.exists() {
return Err(anyhow::anyhow!("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);
copy_file(&src_path, &dest_path)?;
copy_file(src_path, &dest_path)?;
continue;
} else if framework.contains('/') {
return Err(anyhow::anyhow!(
@@ -192,12 +192,8 @@ fn copy_frameworks(dest_dir: &Path, frameworks: &[String]) -> Result<()> {
continue;
}
}
if copy_framework_from(&PathBuf::from("/Library/Frameworks/"), framework, dest_dir)?
|| copy_framework_from(
&PathBuf::from("/Network/Library/Frameworks/"),
framework,
dest_dir,
)?
if copy_framework_from("/Library/Frameworks/".as_ref(), framework, dest_dir)?
|| copy_framework_from("/Network/Library/Frameworks/".as_ref(), framework, dest_dir)?
{
continue;
}
@@ -263,7 +259,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 {
@@ -415,7 +411,8 @@ impl Attributes {
}
pub fn is_dev() -> bool {
env::var("DEP_TAURI_DEV").expect("missing `cargo:dev` instruction, please update tauri to latest")
env::var_os("DEP_TAURI_DEV")
.expect("missing `cargo:dev` instruction, please update tauri to latest")
== "true"
}
@@ -462,7 +459,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
println!("cargo:rerun-if-env-changed=TAURI_CONFIG");
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
let target_os = env::var_os("CARGO_CFG_TARGET_OS").unwrap();
let mobile = target_os == "ios" || target_os == "android";
cfg_alias("desktop", !mobile);
cfg_alias("mobile", mobile);
@@ -499,7 +496,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());
@@ -507,7 +504,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
let cargo_toml_path = Path::new("Cargo.toml").canonicalize()?;
let mut manifest = Manifest::<cargo_toml::Value>::from_path_with_metadata(cargo_toml_path)?;
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
manifest::check(&config, &mut manifest)?;
@@ -533,7 +530,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
ResourcePaths::new(&external_binaries(paths, &target_triple, &target), true),
&target_triple,
target_dir,
manifest.package.as_ref().map(|p| &p.name),
manifest.package.as_ref().map(|p| p.name.as_ref()),
)?;
}
@@ -542,7 +539,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
.bundle
.resources
.clone()
.unwrap_or_else(|| BundleResources::List(Vec::new()));
.unwrap_or(BundleResources::List(Vec::new()));
if target_triple.contains("windows") {
if let Some(fixed_webview2_runtime_path) = match &config.bundle.windows.webview_install_mode {
WebviewInstallMode::FixedRuntime { path } => Some(path),
@@ -573,8 +570,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}");
}
}
}
@@ -589,21 +588,19 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
use semver::Version;
use tauri_winres::{VersionInfo, WindowsResource};
fn find_icon<F: Fn(&&String) -> bool>(config: &Config, predicate: F, default: &str) -> PathBuf {
let icon_path = config
.bundle
.icon
.iter()
.find(|i| predicate(i))
.cloned()
.unwrap_or_else(|| default.to_string());
icon_path.into()
}
let window_icon_path = attributes
.windows_attributes
.window_icon_path
.unwrap_or_else(|| find_icon(&config, |i| i.ends_with(".ico"), "icons/icon.ico"));
.unwrap_or_else(|| {
config
.bundle
.icon
.iter()
.find(|i| i.ends_with(".ico"))
.map(AsRef::as_ref)
.unwrap_or("icons/icon.ico")
.into()
});
let mut res = WindowsResource::new();
@@ -686,7 +683,7 @@ pub fn try_build(attributes: Attributes) -> Result<()> {
}
}
"msvc" => {
if env::var("STATIC_VCRUNTIME").is_ok_and(|v| v == "true") {
if env::var_os("STATIC_VCRUNTIME").is_some_and(|v| v == "true") {
static_vcruntime::build();
}
}

View File

@@ -23,7 +23,7 @@ struct AllowlistedDependency {
name: String,
alias: Option<String>,
kind: DependencyKind,
all_cli_managed_features: Option<Vec<&'static str>>,
all_cli_managed_features: Vec<&'static str>,
expected_features: Vec<String>,
}
@@ -33,7 +33,7 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
name: "tauri-build".into(),
alias: None,
kind: DependencyKind::Build,
all_cli_managed_features: Some(vec!["isolation"]),
all_cli_managed_features: vec!["isolation"],
expected_features: match config.app.security.pattern {
PatternKind::Isolation { .. } => vec!["isolation".to_string()],
_ => vec![],
@@ -43,12 +43,10 @@ pub fn check(config: &Config, manifest: &mut Manifest) -> Result<()> {
name: "tauri".into(),
alias: None,
kind: DependencyKind::Normal,
all_cli_managed_features: Some(
AppConfig::all_features()
.into_iter()
.filter(|f| f != &"tray-icon")
.collect(),
),
all_cli_managed_features: AppConfig::all_features()
.into_iter()
.filter(|f| f != &"tray-icon")
.collect(),
expected_features: config
.app
.features()
@@ -129,23 +127,13 @@ fn check_features(dependency: Dependency, metadata: &AllowlistedDependency) -> R
Dependency::Inherited(dep) => dep.features,
};
let diff = if let Some(all_cli_managed_features) = &metadata.all_cli_managed_features {
features_diff(
&features
.into_iter()
.filter(|f| all_cli_managed_features.contains(&f.as_str()))
.collect::<Vec<String>>(),
&metadata.expected_features,
)
} else {
features_diff(
&features
.into_iter()
.filter(|f| f.starts_with("allow-"))
.collect::<Vec<String>>(),
&metadata.expected_features,
)
};
let diff = features_diff(
&features
.into_iter()
.filter(|f| metadata.all_cli_managed_features.contains(&f.as_str()))
.collect::<Vec<String>>(),
&metadata.expected_features,
);
let mut error_message = String::new();
if !diff.remove.is_empty() {

View File

@@ -2,26 +2,22 @@
// 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();
let mut app_build_gradle = "// THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
val implementation by configurations
dependencies {"
dependencies {
implementation(\"androidx.lifecycle:lifecycle-process:2.10.0\")"
.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 +50,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 +57,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(())
}

View File

@@ -1,5 +1,163 @@
# Changelog
## \[2.8.1]
### Bug Fixes
- [`6252432f0`](https://www.github.com/tauri-apps/tauri/commit/6252432f0757d896d7a61819bbff127efac5a156) ([#14945](https://www.github.com/tauri-apps/tauri/pull/14945) by [@veeceey](https://www.github.com/tauri-apps/tauri/../../veeceey)) Fix WIX installer registry search order so that the named `InstallDir` key takes priority over the NSIS default key, preventing install location from changing on updates.
### What's Changed
- [`7be58a1c6`](https://www.github.com/tauri-apps/tauri/commit/7be58a1c643a7ed6d0f484a7e1134022618eb2b2) ([#14894](https://www.github.com/tauri-apps/tauri/pull/14894) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Log patching bundle type information again
### Dependencies
- Upgraded to `tauri-utils@2.8.3`
## \[2.8.0]
### Enhancements
- [`c769f211f`](https://www.github.com/tauri-apps/tauri/commit/c769f211fcaa543884c9d0f87ebd2ee106c01382) ([#14824](https://www.github.com/tauri-apps/tauri/pull/14824) by [@Kf637](https://www.github.com/tauri-apps/tauri/../../Kf637)) feat(nsis): add Norwegian language support for installer.
### Bug Fixes
- [`7fca58230`](https://www.github.com/tauri-apps/tauri/commit/7fca58230f97c3e6834134419514a0c7dbbe784b) ([#14830](https://www.github.com/tauri-apps/tauri/pull/14830) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Updated `nsis_tauri_utils` to 0.5.3:
- Use an alternative method `CreateProcessWithTokenW` to run programs as user, this fixed a problem that the program launched with the previous method can't query its own handle
### What's Changed
- [`0575dd287`](https://www.github.com/tauri-apps/tauri/commit/0575dd287e021b61d2aedf64d62ae84a2c925fb4) ([#14521](https://www.github.com/tauri-apps/tauri/pull/14521) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Change the way bundle type information is added to binary files. Instead of looking up the value of a variable we simply look for the default value.
### Dependencies
- Upgraded to `tauri-utils@2.8.2`
- Upgraded to `tauri-macos-sign@2.3.3`
## \[2.7.5]
### Enhancements
- [`4176f93ae`](https://www.github.com/tauri-apps/tauri/commit/4176f93ae43ef66714c4934feb3df19df3a3e28a) ([#14570](https://www.github.com/tauri-apps/tauri/pull/14570) by [@chfaft](https://www.github.com/tauri-apps/tauri/../../chfaft)) Consider extensions that are defined in the wxs template.
### Bug Fixes
- [`018b4db22`](https://www.github.com/tauri-apps/tauri/commit/018b4db22e167fa67b37b0933e192a0f3556d3e5) ([#14625](https://www.github.com/tauri-apps/tauri/pull/14625) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Skip signing for NSIS uninstaller when using `--no-sign` flag
- [`91becd9e4`](https://www.github.com/tauri-apps/tauri/commit/91becd9e4fa2db089ddc6b21dadc06133e939e08) ([#14627](https://www.github.com/tauri-apps/tauri/pull/14627) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix NSIS plugins not being signed due to wrong path handlings
### Dependencies
- Upgraded to `tauri-macos-sign@2.3.2`
## \[2.7.4]
### Bug Fixes
- [`1496145f8`](https://www.github.com/tauri-apps/tauri/commit/1496145f8222649efeff22b819a96208670bbea1) ([#14585](https://www.github.com/tauri-apps/tauri/pull/14585) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) Fixed an issue that caused the AppImage bundler to fail with 404 errors for 32-bit builds.
### Performance Improvements
- [`ce98d87ce`](https://www.github.com/tauri-apps/tauri/commit/ce98d87ce0aaa907285852eb80691197424e03c3) ([#14474](https://www.github.com/tauri-apps/tauri/pull/14474) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) refactor: remove needless collect. No user facing changes.
- [`ee3cc4a91`](https://www.github.com/tauri-apps/tauri/commit/ee3cc4a91bf1315ecaefe90f423ffd55ef6c40db) ([#14475](https://www.github.com/tauri-apps/tauri/pull/14475) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) perf: remove needless clones in various files for improved performance. No user facing changes.
### Dependencies
- Upgraded to `tauri-macos-sign@2.3.1`
- Upgraded to `tauri-utils@2.8.1`
- [`b5ef603d8`](https://www.github.com/tauri-apps/tauri/commit/b5ef603d84bd8044625e50dcfdabb099b2e9fdd9) ([#14478](https://www.github.com/tauri-apps/tauri/pull/14478) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Updated NSIS from 3.8 to 3.11
## \[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
- [`f94af9035`](https://www.github.com/tauri-apps/tauri/commit/f94af90359ec8b01138ae542391caa704ec18ca8) ([#13786](https://www.github.com/tauri-apps/tauri/pull/13786) by [@catalinsh](https://www.github.com/tauri-apps/tauri/../../catalinsh)) Fix NSIS per-machine installer not requesting elevation when run by non-admin users.
- [`f2dbe7309`](https://www.github.com/tauri-apps/tauri/commit/f2dbe730979d570be3ee3ecac9621204c4ceb788) ([#13772](https://www.github.com/tauri-apps/tauri/pull/13772) by [@catalinsh](https://www.github.com/tauri-apps/tauri/../../catalinsh)) Fix incorrect expected file path for `nsis_tauri_utils.dll` resulting in tauri-cli re-downloading the file on every build.
- [`7a6fd5b75`](https://www.github.com/tauri-apps/tauri/commit/7a6fd5b75d61071e2771f6277c0376ec206d302a) ([#13863](https://www.github.com/tauri-apps/tauri/pull/13863) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The AppImage bundler now pulls the AppRun binaries from our GitHub mirror, fixing 404 errors.
### Dependencies
- Upgraded to `tauri-utils@2.6.0`
## \[2.5.0]
### New Features

View File

@@ -1,6 +1,6 @@
[package]
name = "tauri-bundler"
version = "2.5.0"
version = "2.8.1"
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.5.0", path = "../tauri-utils", features = [
tauri-utils = { version = "2.8.3", 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"
@@ -43,6 +43,9 @@ dunce = "1"
url = "2"
uuid = { version = "1", features = ["v4", "v5"] }
regex = "1"
goblin = "0.9"
plist = "1"
[target."cfg(target_os = \"windows\")".dependencies]
bitness = "0.4"
@@ -56,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.3", path = "../tauri-macos-sign" }
[target."cfg(target_os = \"linux\")".dependencies]
heck = "0.5"
@@ -66,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"]

View File

@@ -4,6 +4,7 @@
// SPDX-License-Identifier: MIT
mod category;
mod kmp;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
@@ -15,19 +16,82 @@ mod windows;
use tauri_utils::{display_path, platform::Target as TargetPlatform};
const BUNDLE_VAR_TOKEN: &[u8] = b"__TAURI_BUNDLE_TYPE_VAR_UNK";
/// Patch a binary with bundle type information
fn patch_binary(binary: &PathBuf, package_type: &PackageType) -> crate::Result<()> {
#[cfg(target_os = "linux")]
let bundle_type = match package_type {
crate::PackageType::Deb => b"__TAURI_BUNDLE_TYPE_VAR_DEB",
crate::PackageType::Rpm => b"__TAURI_BUNDLE_TYPE_VAR_RPM",
crate::PackageType::AppImage => b"__TAURI_BUNDLE_TYPE_VAR_APP",
// NSIS installers can be built in linux using cargo-xwin
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
_ => {
return Err(crate::Error::InvalidPackageType(
package_type.short_name().to_owned(),
"Linux".to_owned(),
))
}
};
#[cfg(target_os = "windows")]
let bundle_type = match package_type {
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
crate::PackageType::WindowsMsi => b"__TAURI_BUNDLE_TYPE_VAR_MSI",
_ => {
return Err(crate::Error::InvalidPackageType(
package_type.short_name().to_owned(),
"Windows".to_owned(),
))
}
};
#[cfg(target_os = "macos")]
let bundle_type = match package_type {
// NSIS installers can be built in macOS using cargo-xwin
crate::PackageType::Nsis => b"__TAURI_BUNDLE_TYPE_VAR_NSS",
crate::PackageType::MacOsBundle | crate::PackageType::Dmg => {
// skip patching for macOS-native bundles
return Ok(());
}
_ => {
return Err(crate::Error::InvalidPackageType(
package_type.short_name().to_owned(),
"macOS".to_owned(),
))
}
};
log::info!(
"Patching {} with bundle type information: {}",
display_path(binary),
package_type.short_name()
);
let mut file_data = std::fs::read(binary).expect("Could not read binary file.");
let bundle_var_index =
kmp::index_of(BUNDLE_VAR_TOKEN, &file_data).ok_or(crate::Error::MissingBundleTypeVar)?;
file_data[bundle_var_index..bundle_var_index + BUNDLE_VAR_TOKEN.len()]
.copy_from_slice(bundle_type);
std::fs::write(binary, &file_data).map_err(|e| crate::Error::BinaryWriteError(e.to_string()))?;
Ok(())
}
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)]
@@ -55,37 +119,25 @@ 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_binaries_if_needed(settings, target_os)?;
// 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;
}
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);
#[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...");
}
}
// We make a copy of the unsigned main_binary so that we can restore it after each package_type step.
// This allows us to patch the binary correctly and 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
// TODO: change this to work on a copy while preserving the main binary unchanged
let mut main_binary_copy = tempfile::tempfile()?;
let mut main_binary_orignal = std::fs::File::open(&main_binary_path)?;
std::io::copy(&mut main_binary_orignal, &mut main_binary_copy)?;
let mut bundles = Vec::<Bundle>::new();
for package_type in &package_types {
@@ -94,6 +146,15 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
continue;
}
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() {
windows::sign::try_sign(&main_binary_path, settings)?;
}
let bundle_paths = match package_type {
#[cfg(target_os = "macos")]
PackageType::MacOsBundle => macos::app::bundle_project(settings)?,
@@ -114,6 +175,7 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
#[cfg(target_os = "windows")]
PackageType::WindowsMsi => windows::msi::bundle_project(settings, false)?,
// don't restrict to windows as NSIS installers can be built in linux+macOS using cargo-xwin
PackageType::Nsis => windows::nsis::bundle_project(settings, false)?,
#[cfg(target_os = "linux")]
@@ -132,6 +194,14 @@ pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
package_type: package_type.to_owned(),
bundle_paths,
});
// Restore unsigned and unpatched binary
let mut modified_main_binary = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&main_binary_path)?;
main_binary_copy.seek(SeekFrom::Start(0))?;
std::io::copy(&mut main_binary_copy, &mut modified_main_binary)?;
}
if let Some(updater) = settings.updater() {
@@ -181,31 +251,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 {
@@ -230,6 +299,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::warn!("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

View File

@@ -0,0 +1,59 @@
// Copyright 2016-2019 Cargo-Bundle developers <https://github.com/burtonageo/cargo-bundle>
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// KnuthMorrisPratt algorithm
// based on https://github.com/howeih/rust_kmp
pub fn index_of(pattern: &[u8], target: &[u8]) -> Option<usize> {
let failure_function = find_failure_function(pattern);
let mut t_i: usize = 0;
let mut p_i: usize = 0;
let target_len = target.len();
let mut result_idx = None;
let pattern_len = pattern.len();
while (t_i < target_len) && (p_i < pattern_len) {
if target[t_i] == pattern[p_i] {
if result_idx.is_none() {
result_idx.replace(t_i);
}
t_i += 1;
p_i += 1;
if p_i >= pattern_len {
return result_idx;
}
} else {
if p_i == 0 {
p_i = 0;
t_i += 1;
} else {
p_i = failure_function[p_i - 1];
}
result_idx = None;
}
}
None
}
fn find_failure_function(pattern: &[u8]) -> Vec<usize> {
let mut i = 1;
let mut j = 0;
let pattern_length = pattern.len();
let end_i = pattern_length - 1;
let mut failure_function = vec![0usize; pattern_length];
while i <= end_i {
if pattern[i] == pattern[j] {
failure_function[i] = j + 1;
i += 1;
j += 1;
} else if j == 0 {
failure_function[i] = 0;
i += 1;
} else {
j = failure_function[j - 1];
}
}
failure_function
}

View File

@@ -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

View File

@@ -0,0 +1,327 @@
#! /usr/bin/env bash
# GTK3 environment variables: https://docs.gtk.org/gtk3/running.html
# GTK4 environment variables: https://docs.gtk.org/gtk4/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" '{}' \;

View File

@@ -3,13 +3,13 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use super::debian;
use super::{super::debian, write_and_make_executable};
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,16 +223,16 @@ 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/AppImage/AppImageKit/releases/download/continuous/AppRun-{arch}"
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}"
))?;
write_and_make_executable(&apprun, data)?;
}
let linuxdeploy_arch = if arch == "i686" { "i383" } else { arch };
let linuxdeploy_arch = if arch == "i686" { "i386" } 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"))?;
@@ -245,6 +251,21 @@ fn prepare_tools(tools_path: &Path, arch: &str) -> crate::Result<PathBuf> {
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([
@@ -259,12 +280,3 @@ 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<()> {
use std::os::unix::fs::PermissionsExt;
fs::write(path, data)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o770))?;
Ok(())
}

View File

@@ -0,0 +1,25 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
fs,
path::{Path, PathBuf},
};
use crate::Settings;
mod linuxdeploy;
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
linuxdeploy::bundle_project(settings)
}
fn write_and_make_executable(path: &Path, data: Vec<u8>) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
fs::write(path, data)?;
fs::set_permissions(path, fs::Permissions::from_mode(0o770))?;
Ok(())
}

View File

@@ -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 =
@@ -113,8 +119,9 @@ pub fn generate_data(
for bin in settings.binaries() {
let bin_path = settings.binary_path(bin);
fs_utils::copy_file(&bin_path, &bin_dir.join(bin.name()))
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
let trgt = bin_dir.join(bin.name());
fs_utils::copy_file(&bin_path, &trgt)
.with_context(|| format!("Failed to copy binary from {bin_path:?} to {trgt:?}"))?;
}
copy_resource_files(settings, &data_dir).with_context(|| "Failed to copy resource files")?;

View File

@@ -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)]

View File

@@ -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])
}

View File

@@ -23,16 +23,17 @@
// files into the `Contents` directory of the bundle.
use super::{
icon::create_icns_file,
sign::{notarize, notarize_auth, sign, NotarizeAuthError, SignTarget},
icon::{app_icon_name_from_assets_car, create_assets_car_file, create_icns_file},
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,21 +66,29 @@ 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");
let mut sign_paths = Vec::new();
let bundle_icon_file: Option<PathBuf> =
{ create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")? };
let bundle_icon_file =
create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")?;
create_info_plist(&bundle_directory, bundle_icon_file, settings)
.with_context(|| "Failed to create Info.plist")?;
let assets_car_file = create_assets_car_file(&resources_dir, settings)
.with_context(|| "Failed to create app Assets.car")?;
create_info_plist(
&bundle_directory,
bundle_icon_file,
assets_car_file,
settings,
)
.with_context(|| "Failed to create Info.plist")?;
let framework_paths = copy_frameworks_to_bundle(&bundle_directory, settings)
.with_context(|| "Failed to bundle frameworks")?;
@@ -103,7 +112,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,13 +134,17 @@ 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());
log::warn!("skipping app notarization, {e}");
}
}
}
@@ -165,6 +182,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 +196,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(())
@@ -185,6 +212,7 @@ fn copy_custom_files_to_bundle(bundle_directory: &Path, settings: &Settings) ->
fn create_info_plist(
bundle_dir: &Path,
bundle_icon_file: Option<PathBuf>,
assets_car_file: Option<PathBuf>,
settings: &Settings,
) -> crate::Result<()> {
let mut plist = plist::Dictionary::new();
@@ -194,17 +222,6 @@ fn create_info_plist(
"CFBundleExecutable".into(),
settings.main_binary_name()?.into(),
);
if let Some(path) = bundle_icon_file {
plist.insert(
"CFBundleIconFile".into(),
path
.file_name()
.expect("No file name")
.to_string_lossy()
.into_owned()
.into(),
);
}
plist.insert(
"CFBundleIdentifier".into(),
settings.bundle_identifier().into(),
@@ -245,6 +262,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 +318,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
@@ -283,12 +360,34 @@ fn create_info_plist(
);
}
if let Some(path) = bundle_icon_file {
plist.insert(
"CFBundleIconFile".into(),
path
.file_name()
.expect("No file name")
.to_string_lossy()
.into_owned()
.into(),
);
}
if let Some(assets_car_file) = assets_car_file {
if let Some(icon_name) = app_icon_name_from_assets_car(&assets_car_file) {
// only set CFBundleIconName for the Assets.car, CFBundleIconFile is the fallback icns file
plist.insert("CFBundleIconName".into(), icon_name.clone().into());
} else {
log::warn!("Failed to get icon name from Assets.car file");
}
}
if let Some(protocols) = settings.deep_link_protocols() {
plist.insert(
"CFBundleURLTypes".into(),
plist::Value::Array(
protocols
.iter()
.filter(|p| !p.schemes.is_empty())
.map(|protocol| {
let mut dict = plist::Dictionary::new();
dict.insert(
@@ -339,8 +438,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 +474,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 +493,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 +504,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 +522,7 @@ fn copy_frameworks_to_bundle(
{
continue;
}
return Err(crate::Error::GenericError(format!(
return Err(GenericError(format!(
"Could not locate framework: {framework}"
)));
}
@@ -521,3 +615,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."));
}
}

View File

@@ -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,

View File

@@ -4,13 +4,14 @@
// SPDX-License-Identifier: MIT
use crate::bundle::Settings;
use crate::utils::{self, fs_utils};
use crate::utils::{self, fs_utils, CommandExt};
use std::{
cmp::min,
ffi::OsStr,
fs::{self, File},
io::{self, BufWriter},
path::{Path, PathBuf},
process::Command,
};
use image::GenericImageView;
@@ -63,6 +64,11 @@ pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Op
let mut images_to_resize: Vec<(image::DynamicImage, u32, u32)> = vec![];
for icon_path in settings.icon_files() {
let icon_path = icon_path?;
if icon_path.extension().map_or(false, |ext| ext == "car") {
continue;
}
let icon = image::open(&icon_path)?;
let density = if utils::is_retina(&icon_path) { 2 } else { 1 };
let (w, h) = icon.dimensions();
@@ -113,3 +119,206 @@ fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
};
icns::Image::from_data(pixel_format, img.width(), img.height(), img.into_bytes())
}
/// Creates an Assets.car file from a .icon file if there are any in the settings.
/// Uses an existing Assets.car file if it exists in the settings.
/// Returns the path to the Assets.car file.
pub fn create_assets_car_file(
out_dir: &Path,
settings: &Settings,
) -> crate::Result<Option<PathBuf>> {
let Some(icons) = settings.icons() else {
return Ok(None);
};
// If one of the icon files is already a CAR file, just use that.
let mut icon_composer_icon_path = None;
for icon in icons {
let icon_path = Path::new(&icon).to_path_buf();
if icon_path.extension() == Some(OsStr::new("car")) {
let dest_path = out_dir.join("Assets.car");
fs_utils::copy_file(&icon_path, &dest_path)?;
return Ok(Some(dest_path));
}
if icon_path.extension() == Some(OsStr::new("icon")) {
icon_composer_icon_path.replace(icon_path);
}
}
let Some(icon_composer_icon_path) = icon_composer_icon_path else {
return Ok(None);
};
// Check actool version - must be >= 26
if let Some(version) = get_actool_version() {
// Parse the major version number (before the dot)
let major_version: Option<u32> = version.split('.').next().and_then(|s| s.parse().ok());
if let Some(major) = major_version {
if major < 26 {
log::error!("actool version is less than 26, skipping Assets.car file creation. Please update Xcode to 26 or above and try again.");
return Ok(None);
}
} else {
// If we can't parse the version, return None to be safe
log::error!("failed to parse actool version, skipping Assets.car file creation");
return Ok(None);
}
} else {
log::error!("failed to get actool version, skipping Assets.car file creation");
// If we can't get the version, return None to be safe
return Ok(None);
}
// Create a temporary directory for actool work
let temp_dir = tempfile::tempdir()
.map_err(|e| crate::Error::GenericError(format!("failed to create temp dir: {e}")))?;
let icon_dest_path = temp_dir.path().join("Icon.icon");
let output_path = temp_dir.path().join("out");
// Copy the input .icon directory to the temp directory
if icon_composer_icon_path.is_dir() {
fs_utils::copy_dir(&icon_composer_icon_path, &icon_dest_path)?;
} else {
return Err(crate::Error::GenericError(format!(
"{} must be a directory",
icon_composer_icon_path.display()
)));
}
// Create the output directory
fs::create_dir_all(&output_path)?;
// Run actool command
let mut cmd = Command::new("actool");
cmd.arg(&icon_dest_path);
cmd.arg("--compile");
cmd.arg(&output_path);
cmd.arg("--output-format");
cmd.arg("human-readable-text");
cmd.arg("--notices");
cmd.arg("--warnings");
cmd.arg("--output-partial-info-plist");
cmd.arg(output_path.join("assetcatalog_generated_info.plist"));
cmd.arg("--app-icon");
cmd.arg("Icon");
cmd.arg("--include-all-app-icons");
cmd.arg("--accent-color");
cmd.arg("AccentColor");
cmd.arg("--enable-on-demand-resources");
cmd.arg("NO");
cmd.arg("--development-region");
cmd.arg("en");
cmd.arg("--target-device");
cmd.arg("mac");
cmd.arg("--minimum-deployment-target");
cmd.arg("26.0");
cmd.arg("--platform");
cmd.arg("macosx");
cmd.output_ok()?;
let assets_car_path = output_path.join("Assets.car");
if !assets_car_path.exists() {
return Err(crate::Error::GenericError(
"actool did not generate Assets.car file".to_owned(),
));
}
// copy to out_dir
fs_utils::copy_file(&assets_car_path, &out_dir.join("Assets.car"))?;
Ok(Some(out_dir.join("Assets.car")))
}
#[derive(serde::Deserialize)]
struct AssetsCarInfo {
#[serde(rename = "AssetType", default)]
asset_type: String,
#[serde(rename = "Name", default)]
name: String,
}
pub fn app_icon_name_from_assets_car(assets_car_path: &Path) -> Option<String> {
let Ok(output) = Command::new("assetutil")
.arg("--info")
.arg(assets_car_path)
.output_ok()
.inspect_err(|e| log::error!("Failed to get app icon name from Assets.car file: {e}"))
else {
return None;
};
let output = String::from_utf8(output.stdout).ok()?;
let assets_car_info: Vec<AssetsCarInfo> = serde_json::from_str(&output)
.inspect_err(|e| log::error!("Failed to parse Assets.car file info: {e}"))
.ok()?;
assets_car_info
.iter()
.find(|info| info.asset_type == "Icon Image")
.map(|info| info.name.clone())
}
/// Returns the actool short bundle version by running `actool --version --output-format=human-readable-text`.
/// Returns `None` if the command fails or the output cannot be parsed.
pub fn get_actool_version() -> Option<String> {
let Ok(output) = Command::new("actool")
.arg("--version")
.arg("--output-format=human-readable-text")
.output_ok()
.inspect_err(|e| log::error!("Failed to get actool version: {e}"))
else {
return None;
};
let output = String::from_utf8(output.stdout).ok()?;
parse_actool_version(&output)
}
fn parse_actool_version(output: &str) -> Option<String> {
// The output format is:
// /* com.apple.actool.version */
// bundle-version: 24411
// short-bundle-version: 26.1
for line in output.lines() {
let line = line.trim();
if let Some(version) = line.strip_prefix("short-bundle-version:") {
return Some(version.trim().to_string());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_actool_version() {
let output = r#"/* com.apple.actool.version */
some other line
bundle-version: 24411
short-bundle-version: 26.1
another line
"#;
let version = parse_actool_version(output).expect("Failed to parse version");
assert_eq!(version, "26.1");
}
#[test]
fn test_parse_actool_version_missing_fields() {
let output = r#"/* com.apple.actool.version */
bundle-version: 24411
"#;
assert!(parse_actool_version(output).is_none());
}
#[test]
fn test_parse_actool_version_empty() {
assert!(parse_actool_version("").is_none());
}
}

View File

@@ -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?;
@@ -106,7 +106,10 @@ fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<
// Fall back to non-PNG files for any missing sizes.
for icon_path in settings.icon_files() {
let icon_path = icon_path?;
if icon_path.extension() == Some(OsStr::new("png")) {
if icon_path
.extension()
.map_or(false, |ext| ext == "png" || ext == "car")
{
continue;
} else if icon_path.extension() == Some(OsStr::new("icns")) {
let icon_family = icns::IconFamily::read(File::open(&icon_path)?)?;

View File

@@ -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,
@@ -21,13 +21,16 @@ pub fn keychain(identity: Option<&str>) -> crate::Result<Option<tauri_macos_sign
var_os("APPLE_CERTIFICATE"),
var_os("APPLE_CERTIFICATE_PASSWORD"),
) {
// import user certificate - useful for for CI build
// import user certificate - useful 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),
}
}
}

View File

@@ -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::{
@@ -127,7 +126,7 @@ const ALL_PACKAGE_TYPES: &[PackageType] = &[
PackageType::IosBundle,
#[cfg(target_os = "windows")]
PackageType::WindowsMsi,
#[cfg(target_os = "windows")]
// NSIS installers can be built on all platforms but it's hidden in the --help output on macOS/Linux.
PackageType::Nsis,
#[cfg(target_os = "macos")]
PackageType::MacOsBundle,
@@ -232,7 +231,7 @@ pub struct AppImageSettings {
pub struct RpmSettings {
/// The list of RPM dependencies your application relies on.
pub depends: Option<Vec<String>>,
/// the list of of RPM dependencies your application recommends.
/// the list of RPM dependencies your application recommends.
pub recommends: Option<Vec<String>>,
/// The list of RPM dependencies your application provides.
pub provides: Option<Vec<String>>,
@@ -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.
@@ -444,6 +470,13 @@ pub struct NsisSettings {
pub sidebar_image: Option<PathBuf>,
/// The path to an icon file used as the installer icon.
pub installer_icon: Option<PathBuf>,
/// The path to an icon file used as the uninstaller icon.
pub uninstaller_icon: Option<PathBuf>,
/// The path to a bitmap file to display on the header of uninstallers pages.
/// Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`
///
/// The recommended dimensions are 150px x 57px.
pub uninstaller_header_image: Option<PathBuf>,
/// Whether the installation will be for all users or just the current user.
pub install_mode: NSISInstallerMode,
/// A list of installer languages.
@@ -506,6 +539,10 @@ pub struct NsisSettings {
/// Try to ensure that the WebView2 version is equal to or newer than this version,
/// if the user's WebView2 is older than this version,
/// the installer will try to trigger a WebView2 update.
#[deprecated(
since = "2.8.0",
note = "Use `WindowsSettings::minimum_webview2_version` instead."
)]
pub minimum_webview2_version: Option<String>,
}
@@ -561,6 +598,16 @@ pub struct WindowsSettings {
/// if you are on another platform and want to cross-compile and sign you will
/// need to use another tool like `osslsigncode`.
pub sign_command: Option<CustomSignCommandSettings>,
/// Try to ensure that the WebView2 version is equal to or newer than this version,
/// if the user's WebView2 is older than this version,
/// the installer will try to trigger a WebView2 update.
pub minimum_webview2_version: Option<String>,
}
impl WindowsSettings {
pub(crate) fn can_sign(&self) -> bool {
self.sign_command.is_some() || self.certificate_thumbprint.is_some()
}
}
#[allow(deprecated)]
@@ -580,6 +627,7 @@ mod _default {
webview_install_mode: Default::default(),
allow_downgrades: true,
sign_command: None,
minimum_webview2_version: None,
}
}
}
@@ -763,12 +811,16 @@ pub struct Settings {
local_tools_directory: Option<PathBuf>,
/// the bundle settings.
bundle_settings: BundleSettings,
/// Same as `bundle_settings.icon`, but without the .icon directory.
icon_files: Option<Vec<String>>,
/// the binaries to bundle.
binaries: Vec<BundleBinary>,
/// The target platform.
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 +834,7 @@ pub struct SettingsBuilder {
binaries: Vec<BundleBinary>,
target: Option<String>,
local_tools_directory: Option<PathBuf>,
no_sign: bool,
}
impl SettingsBuilder {
@@ -851,6 +904,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.
@@ -864,6 +924,14 @@ impl SettingsBuilder {
};
let target_platform = TargetPlatform::from_triple(&target);
let icon_files = self.bundle_settings.icon.as_ref().map(|paths| {
paths
.iter()
.filter(|p| !p.ends_with(".icon"))
.cloned()
.collect()
});
Ok(Settings {
log_level: self.log_level.unwrap_or(log::Level::Error),
package: self
@@ -883,8 +951,10 @@ impl SettingsBuilder {
.map(|bins| external_binaries(bins, &target, &target_platform)),
..self.bundle_settings
},
icon_files,
target_platform,
target,
no_sign: self.no_sign,
})
}
}
@@ -915,6 +985,11 @@ impl Settings {
&self.target_platform
}
/// Raw list of icons.
pub fn icons(&self) -> Option<&Vec<String>> {
self.bundle_settings.icon.as_ref()
}
/// Returns the architecture for the binary being bundled (e.g. "arm", "x86" or "x86_64").
pub fn binary_arch(&self) -> Arch {
if self.target.starts_with("x86_64") {
@@ -943,7 +1018,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 +1027,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 +1037,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.
@@ -1052,7 +1124,7 @@ impl Settings {
/// Returns an iterator over the icon files to be used for this bundle.
pub fn icon_files(&self) -> ResourcePaths<'_> {
match self.bundle_settings.icon {
match self.icon_files {
Some(ref paths) => ResourcePaths::new(paths.as_slice(), false),
None => ResourcePaths::new(&[], false),
}
@@ -1233,4 +1305,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;
}
}

View File

@@ -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)?;

View File

@@ -5,6 +5,7 @@
#[cfg(target_os = "windows")]
pub mod msi;
pub mod nsis;
pub mod sign;

View File

@@ -70,12 +70,16 @@
<Property Id="ARPURLUPDATEINFO" Value="{{homepage}}"/>
{{/if}}
<!-- NOTE: The order of RegistrySearch elements below matters. In WIX, when multiple
RegistrySearch elements are listed under a single Property, the LAST successful
match wins. We list the NSIS default-key search first and the MSI InstallDir
search second so that the MSI-specific path takes priority when both keys exist. -->
<Property Id="INSTALLDIR">
<!-- First attempt: Search for "InstallDir" -->
<RegistrySearch Id="PrevInstallDirWithName" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw" />
<!-- Second attempt: If the first fails, search for the default key value (this is how the nsis installer currently stores the path) -->
<!-- First attempt: Search for the default key value (this is how the nsis installer stores the path) -->
<RegistrySearch Id="PrevInstallDirNoName" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Type="raw" />
<!-- Second attempt: Search for "InstallDir" which takes priority if found (this is how the msi installer stores the path) -->
<RegistrySearch Id="PrevInstallDirWithName" Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="InstallDir" Type="raw" />
</Property>
<!-- launch app checkbox -->
@@ -279,38 +283,62 @@
{{#if install_webview}}
<!-- WebView2 -->
<Property Id="WVRTINSTALLED">
<RegistrySearch Id="WVRTInstalledSystem" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" Win64="no" />
<RegistrySearch Id="WVRTInstalledUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
<Property Id="INSTALLED_WEBVIEW2_VERSION">
<RegistrySearch Id="Webview2VersionSystemx64" Root="HKLM" Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" />
<RegistrySearch Id="Webview2VersionSystemx86" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw" />
<RegistrySearch Id="Webview2VersionUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Name="pv" Type="raw"/>
</Property>
{{#if download_bootstrapper}}
<!-- Download webview bootstrapper mode -->
<CustomAction Id='DownloadAndInvokeBootstrapper' Directory="INSTALLDIR" Execute="deferred" ExeCommand='powershell.exe -NoProfile -windowstyle hidden try [\{] [\[]Net.ServicePointManager[\]]::SecurityProtocol = [\[]Net.SecurityProtocolType[\]]::Tls12 [\}] catch [\{][\}]; Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=2124703" -OutFile "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" ; Start-Process -FilePath "$env:TEMP\MicrosoftEdgeWebview2Setup.exe" -ArgumentList ({{webview_installer_args}} &apos;/install&apos;) -Wait' Return='check'/>
<InstallExecuteSequence>
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded webview bootstrapper mode -->
{{#if webview2_bootstrapper_path}}
<!-- Embedded webview bootstrapper mode -->
<Binary Id="MicrosoftEdgeWebview2Setup.exe" SourceFile="{{webview2_bootstrapper_path}}"/>
<CustomAction Id='InvokeBootstrapper' BinaryKey='MicrosoftEdgeWebview2Setup.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeBootstrapper' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
<!-- Embedded offline installer -->
{{#if webview2_installer_path}}
<!-- Embedded offline installer -->
<Binary Id="MicrosoftEdgeWebView2RuntimeInstaller.exe" SourceFile="{{webview2_installer_path}}"/>
<CustomAction Id='InvokeStandalone' BinaryKey='MicrosoftEdgeWebView2RuntimeInstaller.exe' Execute="deferred" ExeCommand='{{webview_installer_args}} /install' Return='check' />
<InstallExecuteSequence>
<Custom Action='InvokeStandalone' Before='InstallFinalize'>
<![CDATA[NOT(REMOVE OR WVRTINSTALLED)]]>
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
</Custom>
</InstallExecuteSequence>
{{/if}}
{{#if minimum_webview2_version}}
<!-- Update WebView2 if minimum version requirement not met -->
<Property Id="MINIMUM_WEBVIEW2_VERSION" Value="{{minimum_webview2_version}}" />
<Property Id="EDGEUPDATE_PATH">
<RegistrySearch Id="EdgeUpdateLocalMachine64" Root="HKLM" Key="SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" Name="path" Type="raw" />
<RegistrySearch Id="EdgeUpdateLocalMachine32" Root="HKLM" Key="SOFTWARE\Microsoft\EdgeUpdate" Name="path" Type="raw"/>
<RegistrySearch Id="EdgeUpdateCurrentUser" Root="HKCU" Key="SOFTWARE\Microsoft\EdgeUpdate" Name="path" Type="raw"/>
</Property>
<!-- Chromium updater docs: https://source.chromium.org/chromium/chromium/src/+/main:docs/updater/user_manual.md -->
<!-- Modified from "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView\ModifyPath" -->
<CustomAction Id="UpdateWebView2ViaEdgeUpdate" Execute="deferred" Property="EDGEUPDATE_PATH" Return="check" Impersonate="no" ExeCommand="/install appguid={F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}&amp;needsadmin=true" />
<InstallExecuteSequence>
<Custom Action='UpdateWebView2ViaEdgeUpdate' Before='InstallFinalize'>
<![CDATA[
NOT REMOVE
AND INSTALLED_WEBVIEW2_VERSION
AND (INSTALLED_WEBVIEW2_VERSION < MINIMUM_WEBVIEW2_VERSION)
]]>
</Custom>
</InstallExecuteSequence>
{{/if}}

View File

@@ -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()
@@ -538,6 +532,13 @@ pub fn build_wix_app_installer(
}
}
if let Some(minimum_webview2_version) = &settings.windows().minimum_webview2_version {
data.insert(
"minimum_webview2_version",
to_json(minimum_webview2_version),
);
}
if let Some(license) = settings.license_file() {
if license.ends_with(".rtf") {
data.insert("license", to_json(license));
@@ -703,7 +704,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 {
@@ -757,26 +760,28 @@ pub fn build_wix_app_installer(
}
let main_wxs_path = output_path.join("main.wxs");
fs::write(main_wxs_path, handlebars.render("main.wxs", &data)?)?;
fs::write(&main_wxs_path, handlebars.render("main.wxs", &data)?)?;
let mut candle_inputs = vec![("main.wxs".into(), Vec::new())];
let mut candle_inputs = vec![];
let current_dir = std::env::current_dir()?;
let extension_regex = Regex::new("\"http://schemas.microsoft.com/wix/(\\w+)\"")?;
for fragment_path in fragment_paths {
let fragment_path = current_dir.join(fragment_path);
let fragment_content = fs::read_to_string(&fragment_path)?;
let fragment_handlebars = Handlebars::new();
let fragment = fragment_handlebars.render_template(&fragment_content, &data)?;
let input_paths =
std::iter::once(main_wxs_path).chain(fragment_paths.iter().map(|p| current_dir.join(p)));
for input_path in input_paths {
let input_content = fs::read_to_string(&input_path)?;
let input_handlebars = Handlebars::new();
let input = input_handlebars.render_template(&input_content, &data)?;
let mut extensions = Vec::new();
for cap in extension_regex.captures_iter(&fragment) {
for cap in extension_regex.captures_iter(&input) {
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);
}
candle_inputs.push((fragment_path, extensions));
candle_inputs.push((input_path, extensions));
}
let mut fragment_extensions = HashSet::new();
@@ -785,7 +790,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 +884,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 +993,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 +1081,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)?;
}

View File

@@ -41,6 +41,8 @@ ${StrLoc}
!define INSTALLERICON "{{installer_icon}}"
!define SIDEBARIMAGE "{{sidebar_image}}"
!define HEADERIMAGE "{{header_image}}"
!define UNINSTALLERICON "{{uninstaller_icon}}"
!define UNINSTALLERHEADERIMAGE "{{uninstaller_header_image}}"
!define MAINBINARYNAME "{{main_binary_name}}"
!define MAINBINARYSRCPATH "{{main_binary_path}}"
!define BUNDLEID "{{bundle_id}}"
@@ -95,7 +97,7 @@ VIAddVersionKey "ProductVersion" "${VERSION}"
; Handle install mode, `perUser`, `perMachine` or `both`
!if "${INSTALLMODE}" == "perMachine"
RequestExecutionLevel highest
RequestExecutionLevel admin
!endif
!if "${INSTALLMODE}" == "currentUser"
@@ -129,10 +131,26 @@ VIAddVersionKey "ProductVersion" "${VERSION}"
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
!endif
; Installer header image
; Enable header images for installer and uninstaller pages when either image is configured.
!if "${HEADERIMAGE}" != ""
!define MUI_HEADERIMAGE
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
!else if "${UNINSTALLERHEADERIMAGE}" != ""
!define MUI_HEADERIMAGE
!endif
; Installer header image
!if "${HEADERIMAGE}" != ""
!define MUI_HEADERIMAGE_BITMAP "${HEADERIMAGE}"
!endif
; Uninstaller header image
!if "${UNINSTALLERHEADERIMAGE}" != ""
!define MUI_HEADERIMAGE_UNBITMAP "${UNINSTALLERHEADERIMAGE}"
!endif
; Uninstaller icon
!if "${UNINSTALLERICON}" != ""
!define MUI_UNICON "${UNINSTALLERICON}"
!endif
; Define registry key to store installer language

View File

@@ -0,0 +1,27 @@
LangString addOrReinstall ${LANG_NORWEGIAN} "Legg til/reinstaller komponenter"
LangString alreadyInstalled ${LANG_NORWEGIAN} "Allerede installert"
LangString alreadyInstalledLong ${LANG_NORWEGIAN} "${PRODUCTNAME} ${VERSION} er allerede installert. Velg operasjonen du vil utføre og klikk Neste for å fortsette."
LangString appRunning ${LANG_NORWEGIAN} "{{product_name}} kjører! Lukk den først og prøv igjen."
LangString appRunningOkKill ${LANG_NORWEGIAN} "{{product_name}} kjører!$\nKlikk OK for å avslutte den"
LangString chooseMaintenanceOption ${LANG_NORWEGIAN} "Velg vedlikeholdsoperasjonen som skal utføres."
LangString choowHowToInstall ${LANG_NORWEGIAN} "Velg hvordan du vil installere ${PRODUCTNAME}."
LangString createDesktop ${LANG_NORWEGIAN} "Opprett skrivebordssnarvei"
LangString dontUninstall ${LANG_NORWEGIAN} "Ikke avinstaller"
LangString dontUninstallDowngrade ${LANG_NORWEGIAN} "Ikke avinstaller (nedgradering uten avinstallasjon er deaktivert for denne installasjonen)"
LangString failedToKillApp ${LANG_NORWEGIAN} "Kunne ikke avslutte {{product_name}}. Lukk den først og prøv igjen"
LangString installingWebview2 ${LANG_NORWEGIAN} "Installerer WebView2..."
LangString newerVersionInstalled ${LANG_NORWEGIAN} "En nyere versjon av ${PRODUCTNAME} er allerede installert! Det anbefales ikke at du installerer en eldre versjon. Hvis du virkelig vil installere denne eldre versjonen, er det bedre å avinstallere den nåværende versjonen først. Velg operasjonen du vil utføre og klikk Neste for å fortsette."
LangString older ${LANG_NORWEGIAN} "eldre"
LangString olderOrUnknownVersionInstalled ${LANG_NORWEGIAN} "En $R4-versjon av ${PRODUCTNAME} er installert på systemet ditt. Det anbefales at du avinstallerer den nåværende versjonen før installasjon. Velg operasjonen du vil utføre og klikk Neste for å fortsette."
LangString silentDowngrades ${LANG_NORWEGIAN} "Nedgraderinger er deaktivert for denne installasjonen. Kan ikke fortsette med stille installasjon; bruk den grafiske installasjonen i stedet.$\n"
LangString unableToUninstall ${LANG_NORWEGIAN} "Kunne ikke avinstallere!"
LangString uninstallApp ${LANG_NORWEGIAN} "Avinstaller ${PRODUCTNAME}"
LangString uninstallBeforeInstalling ${LANG_NORWEGIAN} "Avinstaller før installasjon"
LangString unknown ${LANG_NORWEGIAN} "ukjent"
LangString webview2AbortError ${LANG_NORWEGIAN} "Kunne ikke installere WebView2! Appen kan ikke kjøre uten den. Prøv å starte installasjonen på nytt."
LangString webview2DownloadError ${LANG_NORWEGIAN} "Feil: Nedlasting av WebView2 mislyktes - $0"
LangString webview2DownloadSuccess ${LANG_NORWEGIAN} "WebView2-bootstrapper lastet ned"
LangString webview2Downloading ${LANG_NORWEGIAN} "Laster ned WebView2-bootstrapper..."
LangString webview2InstallError ${LANG_NORWEGIAN} "Feil: Installering av WebView2 mislyktes med avslutningskode $1"
LangString webview2InstallSuccess ${LANG_NORWEGIAN} "WebView2 ble installert"
LangString deleteAppData ${LANG_NORWEGIAN} "Slett programdata"

View File

@@ -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.3/nsis_tauri_utils.dll";
const NSIS_TAURI_UTILS_SHA1: &str = "75197FEE3C6A814FE035788D1C34EAD39349B860";
#[cfg(target_os = "windows")]
const NSIS_REQUIRED_FILES: &[&str] = &[
@@ -48,12 +49,15 @@ const NSIS_REQUIRED_FILES: &[&str] = &[
"Bin/makensis.exe",
"Stubs/lzma-x86-unicode",
"Stubs/lzma_solid-x86-unicode",
"Plugins/x86-unicode/nsis_tauri_utils.dll",
"Plugins/x86-unicode/additional/nsis_tauri_utils.dll",
"Include/MUI2.nsh",
"Include/FileFunc.nsh",
"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,9 +297,13 @@ fn build_nsis_app_installer(
);
data.insert("copyright", to_json(settings.copyright_string()));
if settings.can_sign() {
let sign_cmd = format!("{:?}", sign_command("%1", &settings.sign_params())?);
data.insert("uninstaller_sign_cmd", to_json(sign_cmd));
if settings.windows().can_sign() {
if settings.no_sign() {
log::warn!("Skipping signing for NSIS uninstaller due to --no-sign flag.");
} else {
let sign_cmd = format!("{:?}", sign_command("%1", &settings.sign_params())?);
data.insert("uninstaller_sign_cmd", to_json(sign_cmd));
}
}
let version = settings.version_string();
@@ -336,6 +354,20 @@ fn build_nsis_app_installer(
);
}
if let Some(uninstaller_icon) = &nsis.uninstaller_icon {
data.insert(
"uninstaller_icon",
to_json(dunce::canonicalize(uninstaller_icon)?),
);
}
if let Some(uninstaller_header_image) = &nsis.uninstaller_header_image {
data.insert(
"uninstaller_header_image",
to_json(dunce::canonicalize(uninstaller_header_image)?),
);
}
if let Some(installer_hooks) = &nsis.installer_hooks {
let installer_hooks = dunce::canonicalize(installer_hooks)?;
data.insert("installer_hooks", to_json(installer_hooks));
@@ -344,7 +376,12 @@ fn build_nsis_app_installer(
if let Some(start_menu_folder) = &nsis.start_menu_folder {
data.insert("start_menu_folder", to_json(start_menu_folder));
}
if let Some(minimum_webview2_version) = &nsis.minimum_webview2_version {
#[allow(deprecated)]
if let Some(minimum_webview2_version) = nsis
.minimum_webview2_version
.as_ref()
.or(settings.windows().minimum_webview2_version.as_ref())
{
data.insert(
"minimum_webview2_version",
to_json(minimum_webview2_version),
@@ -484,7 +521,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,14 +639,17 @@ fn build_nsis_app_installer(
));
fs::create_dir_all(nsis_installer_path.parent().unwrap())?;
if settings.can_sign() {
log::info!("Signing NSIS plugins");
for dll in NSIS_PLUGIN_FILES {
let path = additional_plugins_path.join(dll);
if path.exists() {
try_sign(&path, settings)?;
} else {
log::warn!("Could not find {}, skipping signing", path.display());
if settings.windows().can_sign() {
if let Some(plugin_copy_path) = &maybe_plugin_copy_path {
let plugin_copy_path = plugin_copy_path.join("x86-unicode");
log::info!("Signing NSIS plugins");
for dll in NSIS_PLUGIN_FILES {
let path = plugin_copy_path.join(dll);
if path.exists() {
try_sign(&path, settings)?;
} else {
log::warn!("Could not find {}, skipping signing", path.display());
}
}
}
}
@@ -636,11 +678,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 +763,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 +788,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 +853,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)
@@ -837,6 +886,7 @@ fn get_lang_data(lang: &str) -> Option<(String, &[u8])> {
"swedish" => include_bytes!("./languages/Swedish.nsh"),
"portuguese" => include_bytes!("./languages/Portuguese.nsh"),
"ukrainian" => include_bytes!("./languages/Ukrainian.nsh"),
"norwegian" => include_bytes!("./languages/Norwegian.nsh"),
_ => return None,
};
Some((path, content))

View File

@@ -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}

View File

@@ -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,38 @@ 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()
.is_some_and(|ext| ext == "exe" || ext == "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)
}
}

View File

@@ -6,10 +6,9 @@ use std::{
fs::create_dir_all,
path::{Path, PathBuf},
};
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 =
@@ -24,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()))
}

View File

@@ -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.
@@ -64,6 +92,22 @@ pub enum Error {
/// Failed to validate downloaded file hash.
#[error("hash mismatch of downloaded file")]
HashError,
/// Failed to parse binary
#[error("Binary parse error: `{0}`")]
BinaryParseError(#[from] goblin::error::Error),
/// Package type is not supported by target platform
#[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")]
MissingBundleTypeVar,
/// Failed to write binary file changed
#[error("Failed to write binary file changes: `{0}`")]
BinaryWriteError(String),
/// Invalid offset while patching binary file
#[deprecated]
#[error("Invalid offset while patching binary file")]
BinaryOffsetOutOfRange,
/// Unsupported architecture.
#[error("Architecture Error: `{0}`")]
ArchError(String),
@@ -118,7 +162,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;

View File

@@ -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)]

View File

@@ -1,5 +1,259 @@
# Changelog
## \[2.10.1]
### Bug Fixes
- [`35c35f27a`](https://www.github.com/tauri-apps/tauri/commit/35c35f27aedc430b602ec74059b271128c15ad36) ([#14931](https://www.github.com/tauri-apps/tauri/pull/14931) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Support comma-separated list of Cargo features on all commands.
- [`0d1cb83ba`](https://www.github.com/tauri-apps/tauri/commit/0d1cb83bab2aa482c7d73116893fd7ff6aa56283) ([#14932](https://www.github.com/tauri-apps/tauri/pull/14932) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix missing Cargo args when running mobile dev and build commands.
- [`33754ae5e`](https://www.github.com/tauri-apps/tauri/commit/33754ae5e3740d022483b6164511c5c001a3c24b) ([#15022](https://www.github.com/tauri-apps/tauri/pull/15022) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fix updater signing private keys generated using `tauri signer generate` with empty password can't be used (The keys generated during tauri were broken between v2.9.3 and v2.10.0, you'll need to regenerate them)
### What's Changed
- [`7be58a1c6`](https://www.github.com/tauri-apps/tauri/commit/7be58a1c643a7ed6d0f484a7e1134022618eb2b2) ([#14894](https://www.github.com/tauri-apps/tauri/pull/14894) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Log patching bundle type information again
### Dependencies
- Upgraded to `tauri-utils@2.8.3`
- Upgraded to `tauri-bundler@2.8.1`
## \[2.10.0]
### Enhancements
- [`f82594410`](https://www.github.com/tauri-apps/tauri/commit/f82594410cd57d6f794f58d4afea0ed335aa796f) ([#13253](https://www.github.com/tauri-apps/tauri/pull/13253) by [@Armaldio](https://www.github.com/tauri-apps/tauri/../../Armaldio)) Allow electron to run the CLI directly
- [`2d28e3143`](https://www.github.com/tauri-apps/tauri/commit/2d28e3143ee3d97d7570ea03877aa00a0d6e47d0) ([#14632](https://www.github.com/tauri-apps/tauri/pull/14632) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Small code refactors for improved code readability. No user facing changes.
- [`a2abe2e6b`](https://www.github.com/tauri-apps/tauri/commit/a2abe2e6bcb9e1eed8484240dfdb76a5bc28ae58) ([#14607](https://www.github.com/tauri-apps/tauri/pull/14607) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Simplified internal representation of `features: Option<Vec<String>>` with `Vec<String>`, no user facing changes
- [`84b04c4a8`](https://www.github.com/tauri-apps/tauri/commit/84b04c4a8d3310b7a7091d10e36244bf94996e51) ([#14759](https://www.github.com/tauri-apps/tauri/pull/14759) by [@amrbashir](https://www.github.com/tauri-apps/tauri/../../amrbashir)) Added new environment variables for `tauri signer sign` command, to align with existing environment variables used in `tauri build`, `tauri bundle` and `tauri signer generate`
- `TAURI_SIGNING_PRIVATE_KEY`
- `TAURI_SIGNING_PRIVATE_KEY_PATH`
- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`
The old environment variables are deprecated and will be removed in a future release.
- `TAURI_PRIVATE_KEY`
- `TAURI_PRIVATE_KEY_PATH`
- `TAURI_PRIVATE_KEY_PASSWORD`
### Bug Fixes
- [`62aa13a12`](https://www.github.com/tauri-apps/tauri/commit/62aa13a124ef46bb5ce9887a2a574dd35ef86d4f) ([#14629](https://www.github.com/tauri-apps/tauri/pull/14629) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix `android build`'s `--aab` and `--apk` flags requiring a value to be provided.
- [`eccff9758`](https://www.github.com/tauri-apps/tauri/commit/eccff97588232055bd0cafd83e6ee03d11a501fb) ([#14779](https://www.github.com/tauri-apps/tauri/pull/14779) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fix empty associated-domains entitlements when domains are not configured for deep links.
- [`ea31b07f1`](https://www.github.com/tauri-apps/tauri/commit/ea31b07f19e0aa467ed0f921f60575cfe09809c8) ([#14789](https://www.github.com/tauri-apps/tauri/pull/14789) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fixed the command description for `tauri inspect`
- [`7fca58230`](https://www.github.com/tauri-apps/tauri/commit/7fca58230f97c3e6834134419514a0c7dbbe784b) ([#14830](https://www.github.com/tauri-apps/tauri/pull/14830) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Updated `nsis_tauri_utils` to 0.5.3:
- Use an alternative method `CreateProcessWithTokenW` to run programs as user, this fixed a problem that the program launched with the previous method can't query its own handle
- [`53611c4d7`](https://www.github.com/tauri-apps/tauri/commit/53611c4d7bdaf89b9a5d7c46a9c4bf4e34216148) ([#14747](https://www.github.com/tauri-apps/tauri/pull/14747) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Only watch dependent workspace members when running `tauri dev` instead of watching on all members
- [`1b0e335d3`](https://www.github.com/tauri-apps/tauri/commit/1b0e335d3f3445948d6590f7e074275d97cd9859) ([#14713](https://www.github.com/tauri-apps/tauri/pull/14713) by [@wasuaje](https://www.github.com/tauri-apps/tauri/../../wasuaje)) `tauri signer sign` doesn't work for files without an extension
### What's Changed
- [`e3fdcb500`](https://www.github.com/tauri-apps/tauri/commit/e3fdcb5002b362b46cde2a1971e4e7f2a1161208) ([#14836](https://www.github.com/tauri-apps/tauri/pull/14836) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Continued refactors of tauri-cli, fix too weak atomics.
- [`0575dd287`](https://www.github.com/tauri-apps/tauri/commit/0575dd287e021b61d2aedf64d62ae84a2c925fb4) ([#14521](https://www.github.com/tauri-apps/tauri/pull/14521) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Change the way bundle type information is added to binary files. Instead of looking up the value of a variable we simply look for the default value.
- [`7f7d9aac2`](https://www.github.com/tauri-apps/tauri/commit/7f7d9aac214e22d9492490543f7a9bcae0a6659e) ([#14668](https://www.github.com/tauri-apps/tauri/pull/14668) by [@sftse](https://www.github.com/tauri-apps/tauri/../../sftse)) Refactored internal use of static on config and directory resolvings, no user facing changes, please report any regressions if you encounter any
### Dependencies
- Upgraded to `tauri-utils@2.8.2`
- Upgraded to `tauri-macos-sign@2.3.3`
- Upgraded to `tauri-bundler@2.8.0`
## \[2.9.6]
### What's Changed
- [`7b1b3514d`](https://www.github.com/tauri-apps/tauri/commit/7b1b3514df771e6e9859b9f54fa4df332433948e) ([#14621](https://www.github.com/tauri-apps/tauri/pull/14621) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Errors like `Error Failed to parse version 2 for for NPM package tauri` when there was no `package-lock.json` file present yet or when using ones like `link:./tauri` are now only logged in `--verbose` mode.
### Dependencies
- Upgraded to `tauri-macos-sign@2.3.2`
- Upgraded to `tauri-bundler@2.7.5`
## \[2.9.5]
### Bug Fixes
- [`f022b2d1a`](https://www.github.com/tauri-apps/tauri/commit/f022b2d1ae57612e39c75782926f2f341d9034a8) ([#14582](https://www.github.com/tauri-apps/tauri/pull/14582) by [@hrzlgnm](https://www.github.com/tauri-apps/tauri/../../hrzlgnm)) Fixed an issue that caused the cli to error out with missing private key, in case the option `--no-sign` was requested and the `tauri.config` has signing key set and the plugin `tauri-plugin-updater` is used.
- [`f855caf8a`](https://www.github.com/tauri-apps/tauri/commit/f855caf8a3830aa5dd6d0b039312866a5d9c3606) ([#14481](https://www.github.com/tauri-apps/tauri/pull/14481) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fixed the mismatched tauri package versions check didn't work for pnpm
- [`79a7d9ec0`](https://www.github.com/tauri-apps/tauri/commit/79a7d9ec01be1a371b8e923848140fea75e9caed) ([#14468](https://www.github.com/tauri-apps/tauri/pull/14468) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) 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.
### Performance Improvements
- [`ce98d87ce`](https://www.github.com/tauri-apps/tauri/commit/ce98d87ce0aaa907285852eb80691197424e03c3) ([#14474](https://www.github.com/tauri-apps/tauri/pull/14474) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) refactor: remove needless collect. No user facing changes.
- [`ee3cc4a91`](https://www.github.com/tauri-apps/tauri/commit/ee3cc4a91bf1315ecaefe90f423ffd55ef6c40db) ([#14475](https://www.github.com/tauri-apps/tauri/pull/14475) by [@Tunglies](https://www.github.com/tauri-apps/tauri/../../Tunglies)) perf: remove needless clones in various files for improved performance. No user facing changes.
### Dependencies
- Upgraded to `tauri-bundler@2.7.4`
- Upgraded to `tauri-macos-sign@2.3.1`
- Upgraded to `tauri-utils@2.8.1`
## \[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
- [`33d079392`](https://www.github.com/tauri-apps/tauri/commit/33d079392ac4a5a153b7d8a6d82fefd6f54a2bdf) ([#13811](https://www.github.com/tauri-apps/tauri/pull/13811) by [@mhbagheri-99](https://www.github.com/tauri-apps/tauri/../../mhbagheri-99)) Allow runner configuration to be an object with cmd, cwd, and args properties. The runner can now be configured as `{ "cmd": "my_runner", "cwd": "/path", "args": ["--quiet"] }` while maintaining backwards compatibility with the existing string format.
### Enhancements
- [`232265c70`](https://www.github.com/tauri-apps/tauri/commit/232265c70e1c213bbb3f84b5541ddc07d330fce1) ([#13209](https://www.github.com/tauri-apps/tauri/pull/13209) by [@kandrelczyk](https://www.github.com/tauri-apps/tauri/../../kandrelczyk)) Binaries are patched before bundling to add the type of a bundle they will placed in. This information will be used during update process to select the correct target.
### Bug Fixes
- [`916aeaa48`](https://www.github.com/tauri-apps/tauri/commit/916aeaa48646a483a78e51cfe1633800ee62c37c) ([#13781](https://www.github.com/tauri-apps/tauri/pull/13781) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Fixes Android dev and build commands reading `tauri.ios.conf.json` instead of `tauri.android.conf.json` to merge platform-specific configuration.
- [`acd757428`](https://www.github.com/tauri-apps/tauri/commit/acd7574284056f9c00894cdce6c07f948fd80c87) ([#13743](https://www.github.com/tauri-apps/tauri/pull/13743) by [@owjs3901](https://www.github.com/tauri-apps/tauri/../../owjs3901)) Fix type of CFBundleVersion generated by `tauri ios init` when `bundleVersion` is a single number (for example `1` instead of `1.0.0`).
- [`0f248b111`](https://www.github.com/tauri-apps/tauri/commit/0f248b111ffb8af934eaf64bd8f4591e628da786) ([#13799](https://www.github.com/tauri-apps/tauri/pull/13799) by [@lucasfernog](https://www.github.com/tauri-apps/tauri/../../lucasfernog)) Install iOS dependencies when needed.
- [`7a6fd5b75`](https://www.github.com/tauri-apps/tauri/commit/7a6fd5b75d61071e2771f6277c0376ec206d302a) ([#13863](https://www.github.com/tauri-apps/tauri/pull/13863) by [@FabianLars](https://www.github.com/tauri-apps/tauri/../../FabianLars)) The AppImage bundler now pulls the AppRun binaries from our GitHub mirror, fixing 404 errors.
- [`bda830410`](https://www.github.com/tauri-apps/tauri/commit/bda8304107da7ca60caaba5674faa793491898c6) ([#13833](https://www.github.com/tauri-apps/tauri/pull/13833) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Fail with an error when trying to migrate from v2 alpha
- [`bda830410`](https://www.github.com/tauri-apps/tauri/commit/bda8304107da7ca60caaba5674faa793491898c6) ([#13833](https://www.github.com/tauri-apps/tauri/pull/13833) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Use v2 stable instead of v2-rc when migrating from v2-beta
### What's Changed
- [`cfc5bb819`](https://www.github.com/tauri-apps/tauri/commit/cfc5bb819637a97141cbda3285c5d772cfc0ebca) ([#13780](https://www.github.com/tauri-apps/tauri/pull/13780) by [@Legend-Master](https://www.github.com/tauri-apps/tauri/../../Legend-Master)) Enable edge to edge in `tauri android init` template
- [`12e359061`](https://www.github.com/tauri-apps/tauri/commit/12e3590613c7b4370aeddc16db7e29335e3a7684) ([#13759](https://www.github.com/tauri-apps/tauri/pull/13759) by [@owjs3901](https://www.github.com/tauri-apps/tauri/../../owjs3901)) Update compileSdk, targetSdk in android template to 36
### Dependencies
- Upgraded to `tauri-bundler@2.5.1`
- Upgraded to `tauri-utils@2.6.0`
## \[2.6.2]
### Bug Fixes

View File

@@ -1,6 +1,6 @@
[package]
name = "tauri-cli"
version = "2.6.2"
version = "2.10.1"
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", default-features = false }
cargo-mobile2 = { version = "0.22.3", default-features = false }
[dependencies]
jsonrpsee = { version = "0.24", features = ["server"] }
@@ -46,29 +46,32 @@ 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.0", default-features = false, path = "../tauri-bundler" }
thiserror = "2"
tauri-bundler = { version = "2.8.1", 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.25", features = ["serde"] }
json-patch = "3"
tauri-utils = { version = "2.5.0", path = "../tauri-utils", features = [
tauri-utils = { version = "2.8.3", path = "../tauri-utils", features = [
"isolation",
"schema",
"config-json5",
"config-toml",
"html-manipulation",
"html-manipulation-2",
] }
toml = "0.8"
jsonschema = "0.30"
toml = "1"
jsonschema = { version = "0.33", default-features = false }
handlebars = "6"
include_dir = "0.7"
dirs = "6"
# 0.7.4 to 0.8.0 were broken, 0.9 pulls in getrandom 0.4 with a high MSRV
# see https://github.com/tauri-apps/tauri/pull/15022
minisign = "=0.7.3"
base64 = "0.22"
ureq = { version = "3", default-features = false, features = ["gzip"] }
@@ -86,8 +89,6 @@ env_logger = "0.11"
icns = { package = "tauri-icns", version = "0.1" }
image = { version = "0.25", default-features = false, features = ["ico"] }
axum = { version = "0.8", features = ["ws"] }
html5ever = "0.29"
kuchiki = { package = "kuchikiki", version = "=0.8.8-speedreader" }
tokio = { version = "1", features = ["macros", "sync"] }
common-path = "1"
serde-value = "0.7"
@@ -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.3", 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"]

View File

@@ -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.

View File

@@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://schema.tauri.app/config/2.6.2",
"$id": "https://schema.tauri.app/config/2.10.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": {
@@ -129,6 +131,7 @@
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": null,
"minimumWebview2Version": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
@@ -163,7 +166,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 +232,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"
},
@@ -260,7 +263,7 @@
"type": "boolean"
},
"x": {
"description": "The horizontal position of the window's top left corner",
"description": "The horizontal position of the window's top left corner in logical pixels",
"type": [
"number",
"null"
@@ -268,7 +271,7 @@
"format": "double"
},
"y": {
"description": "The vertical position of the window's top left corner",
"description": "The vertical position of the window's top left corner in logical pixels",
"type": [
"number",
"null"
@@ -276,19 +279,19 @@
"format": "double"
},
"width": {
"description": "The window width.",
"description": "The window width in logical pixels.",
"default": 800.0,
"type": "number",
"format": "double"
},
"height": {
"description": "The window height.",
"description": "The window height in logical pixels.",
"default": 600.0,
"type": "number",
"format": "double"
},
"minWidth": {
"description": "The min window width.",
"description": "The min window width in logical pixels.",
"type": [
"number",
"null"
@@ -296,7 +299,7 @@
"format": "double"
},
"minHeight": {
"description": "The min window height.",
"description": "The min window height in logical pixels.",
"type": [
"number",
"null"
@@ -304,7 +307,7 @@
"format": "double"
},
"maxWidth": {
"description": "The max window width.",
"description": "The max window width in logical pixels.",
"type": [
"number",
"null"
@@ -312,7 +315,7 @@
"format": "double"
},
"maxHeight": {
"description": "The max window height.",
"description": "The max window height in logical pixels.",
"type": [
"number",
"null"
@@ -365,6 +368,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 +497,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 +574,57 @@
"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"
}
]
},
"activityName": {
"description": "The name of the Android activity to create for this window.",
"type": [
"string",
"null"
]
},
"createdByActivityName": {
"description": "The name of the Android activity that is creating this webview window.\n\n This is important to determine which stack the activity will belong to.",
"type": [
"string",
"null"
]
},
"requestedBySceneIdentifier": {
"description": "Sets the identifier of the scene that is requesting the new scene,\n establishing a relationship between the two scenes.\n\n By default the system uses the foreground scene.",
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
@@ -615,13 +674,13 @@
],
"properties": {
"width": {
"description": "Horizontal margin in physical unit",
"description": "Horizontal margin in physical pixels",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"height": {
"description": "Vertical margin in physical unit",
"description": "Vertical margin in physical pixels",
"type": "integer",
"format": "uint32",
"minimum": 0.0
@@ -884,7 +943,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 +964,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 +1129,14 @@
]
},
{
"description": "A policy where a web view thats 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 thats 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 +1144,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 +1227,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": {
@@ -1240,7 +1318,7 @@
"additionalProperties": false
},
"FsScope": {
"description": "Protocol scope definition.\n It is a list of glob patterns that restrict the API access from the webview.\n\n Each pattern can start with a variable that resolves to a system base directory.\n The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,\n `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,\n `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`,\n `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"description": "Protocol scope definition.\n It is a list of glob patterns that restrict the API access from the webview.\n\n Each pattern can start with a variable that resolves to a system base directory.\n The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`,\n `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`,\n `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$TEMP`,\n `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.",
"anyOf": [
{
"description": "A list of paths that are allowed by this scope.",
@@ -1805,9 +1883,13 @@
"properties": {
"runner": {
"description": "The binary used to build and run the application.",
"type": [
"string",
"null"
"anyOf": [
{
"$ref": "#/definitions/RunnerConfig"
},
{
"type": "null"
}
]
},
"devUrl": {
@@ -1876,15 +1958,62 @@
"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
},
"RunnerConfig": {
"description": "The runner configuration.",
"anyOf": [
{
"description": "A string specifying the binary to run.",
"type": "string"
},
{
"description": "An object with advanced configuration options.",
"type": "object",
"required": [
"cmd"
],
"properties": {
"cmd": {
"description": "The binary to run.",
"type": "string"
},
"cwd": {
"description": "The current working directory to run the command from.",
"type": [
"string",
"null"
]
},
"args": {
"description": "Arguments to pass to the command.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
}
}
]
},
"FrontendDist": {
"description": "Defines the URL or assets to embed in the application.",
"anyOf": [
{
"description": "An external URL that should be used as the default application URL.",
"description": "An external URL that should be used as the default application URL. No assets are embedded in the app in this case.",
"type": "string",
"format": "uri"
},
@@ -1893,7 +2022,7 @@
"type": "string"
},
{
"description": "An array of files to embed on the app.",
"description": "An array of files to embed in the app.",
"type": "array",
"items": {
"type": "string"
@@ -2014,7 +2143,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"
@@ -2053,7 +2182,7 @@
]
},
"fileAssociations": {
"description": "File associations to application.",
"description": "File types to associate with the application.",
"type": [
"array",
"null"
@@ -2097,6 +2226,7 @@
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": null,
"minimumWebview2Version": null,
"nsis": null,
"signCommand": null,
"timestampUrl": null,
@@ -2165,7 +2295,7 @@
"iOS": {
"description": "iOS configuration.",
"default": {
"minimumSystemVersion": "13.0"
"minimumSystemVersion": "14.0"
},
"allOf": [
{
@@ -2176,6 +2306,7 @@
"android": {
"description": "Android configuration.",
"default": {
"autoIncrementVersionCode": false,
"minSdkVersion": 24
},
"allOf": [
@@ -2327,6 +2458,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": [
@@ -2365,6 +2506,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
@@ -2446,6 +2598,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",
@@ -2493,6 +2669,13 @@
"default": true,
"type": "boolean"
},
"minimumWebview2Version": {
"description": "Try to ensure that the WebView2 version is equal to or newer than this version,\n if the user's WebView2 is older than this version,\n the installer will try to trigger a WebView2 update.",
"type": [
"string",
"null"
]
},
"wix": {
"description": "Configuration for the MSI generated with WiX.",
"anyOf": [
@@ -2639,7 +2822,7 @@
"type": "object",
"properties": {
"version": {
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\n Because a valid version is required for MSI installer, it will be derived from [`Config::version`] if this field is not set.\n\n The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.\n The third and foruth fields have a maximum value of 65,535.\n\n See <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
"description": "MSI installer version in the format `major.minor.patch.build` (build is optional).\n\n Because a valid version is required for MSI installer, it will be derived from [`Config::version`] if this field is not set.\n\n The first field is the major version and has a maximum value of 255. The second field is the minor version and has a maximum value of 255.\n The third and fourth fields have a maximum value of 65,535.\n\n See <https://learn.microsoft.com/en-us/windows/win32/msi/productversion> for more info.",
"type": [
"string",
"null"
@@ -2735,6 +2918,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
@@ -2808,6 +2996,20 @@
"null"
]
},
"uninstallerIcon": {
"description": "The path to an icon file used as the uninstaller icon.",
"type": [
"string",
"null"
]
},
"uninstallerHeaderImage": {
"description": "The path to a bitmap file to display on the header of uninstallers pages.\n Defaults to [`Self::header_image`]. If this is set but [`Self::header_image`] is not, a default image from NSIS will be applied to `header_image`\n\n The recommended dimensions are 150px x 57px.",
"type": [
"string",
"null"
]
},
"installMode": {
"description": "Whether the installation will be for all users or just the current user.",
"default": "currentUser",
@@ -2859,14 +3061,15 @@
]
},
"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"
]
},
"minimumWebview2Version": {
"description": "Try to ensure that the WebView2 version is equal to or newer than this version,\n if the user's WebView2 is older than this version,\n the installer will try to trigger a WebView2 update.",
"description": "Deprecated: use [`WindowsConfig::minimum_webview2_version`] (`bundle > windows > minimumWebview2Version`) instead.\n\n Try to ensure that the WebView2 version is equal to or newer than this version,\n if the user's WebView2 is older than this version,\n the installer will try to trigger a WebView2 update.",
"deprecated": true,
"type": [
"string",
"null"
@@ -2938,7 +3141,7 @@
"description": "Custom Signing Command configuration.",
"anyOf": [
{
"description": "A string notation of the script to execute.\n\n \"%1\" will be replaced with the path to the binary to be signed.\n\n This is a simpler notation for the command.\n Tauri will split the string with `' '` and use the first element as the command name and the rest as arguments.\n\n If you need to use whitespace in the command or arguments, use the object notation [`Self::ScriptWithOptions`].",
"description": "A string notation of the script to execute.\n\n \"%1\" will be replaced with the path to the binary to be signed.\n\n This is a simpler notation for the command.\n Tauri will split the string with `' '` and use the first element as the command name and the rest as arguments.\n\n If you need to use whitespace in the command or arguments, use the object notation [`Self::CommandWithOptions`].",
"type": "string"
},
{
@@ -3421,7 +3624,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",
@@ -3461,6 +3664,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": {
@@ -3630,8 +3840,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
@@ -3656,6 +3873,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

View File

@@ -1,9 +1,9 @@
{
"cli.js": {
"version": "2.6.2",
"version": "2.10.1",
"node": ">= 10.0.0"
},
"tauri": "2.6.2",
"tauri-build": "2.3.0",
"tauri-plugin": "2.3.0"
"tauri": "2.10.3",
"tauri-build": "2.5.6",
"tauri-plugin": "2.5.4"
}

View File

@@ -2330,6 +2330,14 @@
"string",
"null"
]
},
"fipsCompliant": {
"description": "Enables FIPS compliant algorithms.",
"default": null,
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false

View File

@@ -7,11 +7,7 @@ use std::{collections::HashSet, path::PathBuf};
use clap::Parser;
use tauri_utils::acl::capability::{Capability, PermissionEntry};
use crate::{
acl::FileFormat,
helpers::{app_paths::tauri_dir, prompts},
Result,
};
use crate::{acl::FileFormat, error::ErrorExt, helpers::prompts, Result};
#[derive(Debug, Parser)]
#[clap(about = "Create a new permission file")]
@@ -36,7 +32,7 @@ pub struct Options {
}
pub fn command(options: Options) -> Result<()> {
crate::helpers::app_paths::resolve();
let dirs = crate::helpers::app_paths::resolve_dirs();
let identifier = match options.identifier {
Some(i) => i,
@@ -106,10 +102,11 @@ 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");
let capabilities_dir = dirs.tauri.join("capabilities");
capabilities_dir.join(format!(
"{}.{}",
capability.identifier,
@@ -125,17 +122,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());

View File

@@ -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)
}

View File

@@ -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(())

View File

@@ -4,7 +4,10 @@
use clap::Parser;
use crate::{helpers::app_paths::tauri_dir, Result};
use crate::{
error::{Context, ErrorExt},
Result,
};
use colored::Colorize;
use tauri_utils::acl::{manifest::Manifest, APP_ACL_KEY};
@@ -21,16 +24,19 @@ pub struct Options {
}
pub fn command(options: Options) -> Result<()> {
crate::helpers::app_paths::resolve();
let dirs = crate::helpers::app_paths::resolve_dirs();
let acl_manifests_path = tauri_dir()
let acl_manifests_path = dirs
.tauri
.join("gen")
.join("schemas")
.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")
}
}

View File

@@ -8,6 +8,7 @@ use clap::Parser;
use crate::{
acl::FileFormat,
error::{Context, ErrorExt},
helpers::{app_paths::resolve_tauri_dir, prompts},
Result,
};
@@ -23,10 +24,10 @@ pub struct Options {
#[clap(long)]
description: Option<String>,
/// List of commands to allow
#[clap(short, long, use_value_delimiter = true)]
#[clap(short, long, value_delimiter = ',')]
allow: Option<Vec<String>>,
/// List of commands to deny
#[clap(short, long, use_value_delimiter = true)]
#[clap(short, long, value_delimiter = ',')]
deny: Option<Vec<String>>,
/// Output file format.
#[clap(long, default_value_t = FileFormat::Json)]
@@ -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());

View File

@@ -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)?;
}

View File

@@ -8,8 +8,9 @@ use regex::Regex;
use crate::{
acl,
error::ErrorExt,
helpers::{
app_paths::{resolve_frontend_dir, tauri_dir},
app_paths::{resolve_frontend_dir, Dirs},
cargo,
npm::PackageManager,
},
@@ -38,11 +39,11 @@ pub struct Options {
}
pub fn command(options: Options) -> Result<()> {
crate::helpers::app_paths::resolve();
run(options)
let dirs = crate::helpers::app_paths::resolve_dirs();
run(options, &dirs)
}
pub fn run(options: Options) -> Result<()> {
pub fn run(options: Options, dirs: &Dirs) -> Result<()> {
let (plugin, version) = options
.plugin
.split_once('@')
@@ -64,13 +65,12 @@ 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"
);
}
let frontend_dir = resolve_frontend_dir();
let tauri_dir = tauri_dir();
let target_str = metadata
.desktop_only
@@ -89,7 +89,7 @@ pub fn run(options: Options) -> Result<()> {
branch: options.branch.as_deref(),
rev: options.rev.as_deref(),
tag: options.tag.as_deref(),
cwd: Some(tauri_dir),
cwd: Some(dirs.tauri),
target: target_str,
})?;
@@ -114,9 +114,9 @@ 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)?;
manager.install(&[npm_spec], dirs.tauri)?;
}
let _ = acl::permission::add::command(acl::permission::add::Options {
@@ -130,6 +130,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 +141,13 @@ 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*)")?;
for file in [tauri_dir.join("src/main.rs"), tauri_dir.join("src/lib.rs")] {
let contents = std::fs::read_to_string(&file)?;
let re = Regex::new(r"(tauri\s*::\s*Builder\s*::\s*default\(\))(\s*)").unwrap();
for file in [
dirs.tauri.join("src/main.rs"),
dirs.tauri.join("src/lib.rs"),
] {
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,14 +161,14 @@ 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
log::info!("Running `cargo fmt`...");
let _ = Command::new("cargo")
.arg("fmt")
.current_dir(tauri_dir)
.current_dir(dirs.tauri)
.status();
}

View File

@@ -4,17 +4,19 @@
use crate::{
bundle::BundleFormat,
error::{Context, ErrorExt},
helpers::{
self,
app_paths::tauri_dir,
config::{get as get_config, ConfigHandle, FrontendDist},
app_paths::Dirs,
config::{get_config, ConfigMetadata, FrontendDist},
},
interface::{rust::get_cargo_target_dir, AppInterface, Interface},
info::plugins::check_mismatched_packages,
interface::{rust::get_cargo_target_dir, AppInterface},
ConfigValue, Result,
};
use anyhow::Context;
use clap::{ArgAction, Parser};
use std::env::set_current_dir;
use tauri_utils::config::RunnerConfig;
use tauri_utils::platform::Target;
#[derive(Debug, Clone, Parser)]
@@ -25,7 +27,7 @@ use tauri_utils::platform::Target;
pub struct Options {
/// Binary to use to build the application, defaults to `cargo`
#[clap(short, long)]
pub runner: Option<String>,
pub runner: Option<RunnerConfig>,
/// Builds with the debug flag
#[clap(short, long)]
pub debug: bool,
@@ -37,8 +39,8 @@ pub struct Options {
#[clap(short, long)]
pub target: Option<String>,
/// Space or comma separated list of features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
/// Space or comma separated list of bundles to package.
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub bundles: Option<Vec<BundleFormat>>,
@@ -59,10 +61,32 @@ 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();
let dirs = crate::helpers::app_paths::resolve_dirs();
if options.no_sign {
log::warn!("--no-sign flag detected: Signing will be skipped.");
}
let ci = options.ci;
@@ -75,37 +99,37 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
let mut interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),
options.target.clone(),
)?;
let mut interface = AppInterface::new(&config, options.target.clone(), dirs.tauri)?;
setup(&interface, &mut options, config.clone(), false)?;
setup(&interface, &mut options, &config, &dirs, false)?;
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();
let out_dir = app_settings.out_dir(&interface_options)?;
let out_dir = app_settings.out_dir(&interface_options, dirs.tauri)?;
let bin_path = interface.build(interface_options)?;
let bin_path = interface.build(interface_options, &dirs)?;
log::info!(action ="Built"; "application at: {}", tauri_utils::display_path(bin_path));
log::info!(action = "Built"; "application at: {}", tauri_utils::display_path(bin_path));
let app_settings = interface.app_settings();
if !options.no_bundle && (config_.bundle.active || options.bundles.is_some()) {
if !options.no_bundle && (config.bundle.active || options.bundles.is_some()) {
crate::bundle::bundle(
&options.into(),
verbosity,
ci,
&interface,
&app_settings,
config_,
&*app_settings,
&config,
&dirs,
&out_dir,
)?;
}
@@ -116,73 +140,87 @@ pub fn command(mut options: Options, verbosity: u8) -> Result<()> {
pub fn setup(
interface: &AppInterface,
options: &mut Options,
config: ConfigHandle,
config: &ConfigMetadata,
dirs: &Dirs,
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(dirs.frontend, dirs.tauri) {
if options.ignore_version_mismatches {
log::error!("{error}");
} else {
return Err(error);
}
}
let config_guard = config.lock().unwrap();
let config_ = config_guard.as_ref().unwrap();
set_current_dir(dirs.tauri).context("failed to set current directory")?;
let bundle_identifier_source = config_
let bundle_identifier_source = config
.find_bundle_identifier_overwriter()
.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
if config.identifier == "com.tauri.dev" {
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_
if config
.identifier
.chars()
.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '.'))
{
log::error!(
"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
crate::error::bail!(
"The bundle identifier \"{}\" set in `{bundle_identifier_source:?} identifier`. The bundle identifier string must contain only alphanumeric characters (A-Z, a-z, and 0-9), hyphens (-), and periods (.).",
config.identifier,
);
std::process::exit(1);
}
if config_.identifier.ends_with(".app") {
if config.identifier.ends_with(".app") {
log::warn!(
"The bundle identifier \"{}\" set in `{} identifier` ends with `.app`. This is not recommended because it conflicts with the application bundle extension on macOS.",
config_.identifier,
bundle_identifier_source
"The bundle identifier \"{}\" set in `{bundle_identifier_source:?} identifier` ends with `.app`. This is not recommended because it conflicts with the application bundle extension on macOS.",
config.identifier,
);
}
if let Some(before_build) = config_.build.before_build_command.clone() {
helpers::run_hook("beforeBuildCommand", before_build, interface, options.debug)?;
if let Some(before_build) = config.build.before_build_command.clone() {
helpers::run_hook(
"beforeBuildCommand",
before_build,
interface,
options.debug,
dirs.frontend,
)?;
}
if let Some(FrontendDist::Directory(web_asset_path)) = &config_.build.frontend_dist {
if let Some(FrontendDist::Directory(web_asset_path)) = &config.build.frontend_dist {
if !web_asset_path.exists() {
let absolute_path = web_asset_path
.parent()
.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
// https://github.com/tauri-apps/tauri/issues/13287
let target_path = get_cargo_target_dir(&options.args)?;
let target_path = get_cargo_target_dir(&options.args, dirs.tauri)?;
let mut out_folders = Vec::new();
if let Ok(web_asset_canonical) = dunce::canonicalize(web_asset_path) {
if let Ok(relative_path) = target_path.strip_prefix(&web_asset_canonical) {
@@ -201,22 +239,21 @@ 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" }
));
);
}
}
if options.runner.is_none() {
options.runner.clone_from(&config_.build.runner);
options.runner = config.build.runner.clone();
}
options
.features
.get_or_insert(Vec::new())
.extend(config_.build.features.clone().unwrap_or_default());
.extend_from_slice(config.build.features.as_deref().unwrap_or_default());
interface.build_options(&mut options.args, &mut options.features, mobile);
Ok(())

View File

@@ -8,19 +8,19 @@ 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,
config::{get as get_config, ConfigMetadata},
app_paths::Dirs,
config::{get_config, ConfigMetadata},
updater_signature,
},
interface::{AppInterface, AppSettings, Interface},
interface::{AppInterface, AppSettings},
ConfigValue,
};
@@ -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}"))
}
}
@@ -43,7 +43,7 @@ impl ValueEnum for BundleFormat {
}
fn to_possible_value(&self) -> Option<PossibleValue> {
let hide = self.0 == PackageType::Updater;
let hide = (!cfg!(windows) && self.0 == PackageType::Nsis) || self.0 == PackageType::Updater;
Some(PossibleValue::new(self.0.short_name()).hide(hide))
}
}
@@ -70,8 +70,8 @@ pub struct Options {
#[clap(short, long)]
pub config: Vec<ConfigValue>,
/// Space or comma separated list of features, should be the same features passed to `tauri build` if any.
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
/// Target triple to build against.
///
/// It must be one of the values outputted by `$rustc --print target-list` or `universal-apple-darwin` for an universal macOS application.
@@ -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,12 +111,14 @@ 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,
}
}
}
pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
crate::helpers::app_paths::resolve();
let dirs = crate::helpers::app_paths::resolve_dirs();
let ci = options.ci;
@@ -111,32 +131,30 @@ pub fn command(options: Options, verbosity: u8) -> crate::Result<()> {
let config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
let interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),
options.target.clone(),
)?;
let interface = AppInterface::new(&config, options.target.clone(), dirs.tauri)?;
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(dirs.tauri).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();
let out_dir = app_settings.out_dir(&interface_options)?;
let out_dir = app_settings.out_dir(&interface_options, dirs.tauri)?;
bundle(
&options,
verbosity,
ci,
&interface,
&app_settings,
config_,
&*app_settings,
&config,
&dirs,
&out_dir,
)
}
@@ -147,8 +165,9 @@ pub fn bundle<A: AppSettings>(
verbosity: u8,
ci: bool,
interface: &AppInterface,
app_settings: &std::sync::Arc<A>,
app_settings: &A,
config: &ConfigMetadata,
dirs: &Dirs,
out_dir: &Path,
) -> crate::Result<()> {
let package_types: Vec<PackageType> = if let Some(bundles) = &options.bundles {
@@ -175,13 +194,21 @@ pub fn bundle<A: AppSettings>(
before_bundle,
interface,
options.debug,
dirs.frontend,
)?;
}
}
let mut settings = app_settings
.get_bundler_settings(options.clone().into(), config, out_dir, package_types)
.get_bundler_settings(
options.clone().into(),
config,
out_dir,
package_types,
dirs.tauri,
)
.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 +216,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)?;
@@ -230,12 +252,18 @@ fn sign_updaters(
return Ok(());
}
if settings.no_sign() {
log::warn!("Updater signing is skipped due to --no-sign flag.");
return Ok(());
}
// get the public key
let pubkey = &update_settings.pubkey;
// 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,15 +275,21 @@ 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
};
if password.is_none() {
log::info!("Decrypting updater signing key, expect a prompt for password")
}
let secret_key =
updater_signature::secret_key(private_key, password).context("failed to decode secret key")?;
let public_key = updater_signature::pub_key(pubkey).context("failed to decode pubkey")?;
@@ -290,11 +324,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}");
}

View File

@@ -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}");
}

View File

@@ -3,36 +3,36 @@
// SPDX-License-Identifier: MIT
use crate::{
error::{Context, ErrorExt},
helpers::{
app_paths::{frontend_dir, tauri_dir},
app_paths::Dirs,
command_env,
config::{
get as get_config, reload as reload_config, BeforeDevCommand, ConfigHandle, FrontendDist,
},
config::{get_config, reload_config, BeforeDevCommand, ConfigMetadata, FrontendDist},
},
interface::{AppInterface, ExitReason, Interface},
CommandExt, ConfigValue, Result,
info::plugins::check_mismatched_packages,
interface::{AppInterface, ExitReason},
CommandExt, ConfigValue, Error, Result,
};
use anyhow::{bail, Context};
use clap::{ArgAction, Parser};
use shared_child::SharedChild;
use tauri_utils::platform::Target;
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},
Arc, Mutex, OnceLock,
OnceLock,
},
};
mod builtin_dev_server;
static BEFORE_DEV: OnceLock<Mutex<Arc<SharedChild>>> = OnceLock::new();
static KILL_BEFORE_DEV_FLAG: OnceLock<AtomicBool> = OnceLock::new();
static BEFORE_DEV: OnceLock<SharedChild> = OnceLock::new();
static KILL_BEFORE_DEV_FLAG: AtomicBool = AtomicBool::new(false);
#[cfg(unix)]
const KILL_CHILDREN_SCRIPT: &[u8] = include_bytes!("../scripts/kill-children.sh");
@@ -49,13 +49,13 @@ pub const TAURI_CLI_BUILTIN_WATCHER_IGNORE_FILE: &[u8] =
pub struct Options {
/// Binary to use to run the application
#[clap(short, long)]
pub runner: Option<String>,
pub runner: Option<RunnerConfig>,
/// Target triple to build against
#[clap(short, long)]
pub target: Option<String>,
/// List of cargo features to activate
#[clap(short, long, action = ArgAction::Append, num_args(0..))]
pub features: Option<Vec<String>>,
#[clap(short, long, action = ArgAction::Append, num_args(0..), value_delimiter = ',')]
pub features: Vec<String>,
/// Exit on panic
#[clap(short, long)]
pub exit_on_panic: bool,
@@ -81,6 +81,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)]
@@ -94,54 +97,57 @@ pub struct Options {
}
pub fn command(options: Options) -> Result<()> {
crate::helpers::app_paths::resolve();
let dirs = crate::helpers::app_paths::resolve_dirs();
let r = command_internal(options);
let r = command_internal(options, dirs);
if r.is_err() {
kill_before_dev_process();
}
r
}
fn command_internal(mut options: Options) -> Result<()> {
fn command_internal(mut options: Options, dirs: Dirs) -> Result<()> {
let target = options
.target
.as_deref()
.map(Target::from_triple)
.unwrap_or_else(Target::current);
let config = get_config(
let mut config = get_config(
target,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
let mut interface = AppInterface::new(
config.lock().unwrap().as_ref().unwrap(),
options.target.clone(),
)?;
let mut interface = AppInterface::new(&config, options.target.clone(), dirs.tauri)?;
setup(&interface, &mut options, config)?;
setup(&interface, &mut options, &mut config, &dirs)?;
let exit_on_panic = options.exit_on_panic;
let no_watch = options.no_watch;
interface.dev(options.into(), move |status, reason| {
on_app_exit(status, reason, exit_on_panic, no_watch)
})
interface.dev(
&mut config,
options.into(),
move |status, reason| on_app_exit(status, reason, exit_on_panic, no_watch),
&dirs,
)
}
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")?;
pub fn setup(
interface: &AppInterface,
options: &mut Options,
config: &mut ConfigMetadata,
dirs: &Dirs,
) -> Result<()> {
std::thread::spawn(|| {
if let Err(error) = check_mismatched_packages(dirs.frontend, dirs.tauri) {
log::error!("{error}");
}
});
if let Some(before_dev) = config
.lock()
.unwrap()
.as_ref()
.unwrap()
.build
.before_dev_command
.clone()
{
set_current_dir(dirs.tauri).context("failed to set current directory")?;
if let Some(before_dev) = config.build.before_dev_command.clone() {
let (script, script_cwd, wait) = match before_dev {
BeforeDevCommand::Script(s) if s.is_empty() => (None, None, false),
BeforeDevCommand::Script(s) => (Some(s), None, false),
@@ -149,7 +155,7 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
(Some(script), cwd.map(Into::into), wait)
}
};
let cwd = script_cwd.unwrap_or_else(|| frontend_dir().clone());
let cwd = script_cwd.unwrap_or_else(|| dirs.frontend.to_owned());
if let Some(before_dev) = script {
log::info!(action = "Running"; "BeforeDevCommand (`{}`)", before_dev);
let mut env = command_env(true);
@@ -178,15 +184,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,27 +200,23 @@ 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}`"));
let child = Arc::new(child);
let child_ = child.clone();
let child = BEFORE_DEV.get_or_init(move || child);
std::thread::spawn(move || {
let status = child_
let status = child
.wait()
.expect("failed to wait on \"beforeDevCommand\"");
if !(status.success() || KILL_BEFORE_DEV_FLAG.get().unwrap().load(Ordering::Relaxed)) {
if !(status.success() || KILL_BEFORE_DEV_FLAG.load(Ordering::SeqCst)) {
log::error!("The \"beforeDevCommand\" terminated with a non-zero status code.");
exit(status.code().unwrap_or(1));
}
});
BEFORE_DEV.set(Mutex::new(child)).unwrap();
KILL_BEFORE_DEV_FLAG.set(AtomicBool::default()).unwrap();
let _ = ctrlc::set_handler(move || {
kill_before_dev_process();
exit(130);
@@ -224,50 +226,25 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
}
if options.runner.is_none() {
options
.runner
.clone_from(&config.lock().unwrap().as_ref().unwrap().build.runner);
options.runner = config.build.runner.clone();
}
let mut cargo_features = config
.lock()
.unwrap()
.as_ref()
.unwrap()
.build
.features
.clone()
.unwrap_or_default();
if let Some(features) = &options.features {
cargo_features.extend(features.clone());
}
let mut cargo_features = config.build.features.clone().unwrap_or_default();
cargo_features.extend(options.features.clone());
let mut dev_url = config
.lock()
.unwrap()
.as_ref()
.unwrap()
.build
.dev_url
.clone();
let frontend_dist = config
.lock()
.unwrap()
.as_ref()
.unwrap()
.build
.frontend_dist
.clone();
let mut dev_url = config.build.dev_url.clone();
let frontend_dist = config.build.frontend_dist.clone();
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 ip = options.host.unwrap_or_else(|| Ipv4Addr::LOCALHOST.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());
@@ -277,25 +254,27 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
}
})));
reload_config(&options.config.iter().map(|c| &c.0).collect::<Vec<_>>())?;
reload_config(
config,
&options.config.iter().map(|c| &c.0).collect::<Vec<_>>(),
dirs.tauri,
)?;
}
}
}
if !options.no_dev_server_wait {
if let Some(url) = dev_url {
let host = url
.host()
.unwrap_or_else(|| panic!("No host name in the URL"));
let host = url.host().expect("No host name in the URL");
let port = url
.port_or_known_default()
.unwrap_or_else(|| panic!("No port number in the URL"));
.expect("No port number in the URL");
let addrs;
let addr;
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) => {
@@ -331,6 +310,12 @@ pub fn setup(interface: &AppInterface, options: &mut Options, config: ConfigHand
}
}
if options.additional_watch_folders.is_empty() {
options
.additional_watch_folders
.extend(config.build.additional_watch_folders.clone());
}
Ok(())
}
@@ -346,12 +331,10 @@ pub fn on_app_exit(code: Option<i32>, reason: ExitReason, exit_on_panic: bool, n
pub fn kill_before_dev_process() {
if let Some(child) = BEFORE_DEV.get() {
let child = child.lock().unwrap();
let kill_before_dev_flag = KILL_BEFORE_DEV_FLAG.get().unwrap();
if kill_before_dev_flag.load(Ordering::Relaxed) {
if KILL_BEFORE_DEV_FLAG.load(Ordering::SeqCst) {
return;
}
kill_before_dev_flag.store(true, Ordering::Relaxed);
KILL_BEFORE_DEV_FLAG.store(true, Ordering::SeqCst);
#[cfg(windows)]
{
let powershell_path = std::env::var("SYSTEMROOT").map_or_else(

View File

@@ -7,8 +7,6 @@ use axum::{
http::{header, StatusCode, Uri},
response::{IntoResponse, Response},
};
use html5ever::{namespace_url, ns, LocalName, QualName};
use kuchiki::{traits::TendrilSink, NodeRef};
use std::{
net::{IpAddr, SocketAddr},
path::{Path, PathBuf},
@@ -18,6 +16,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 +29,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 +38,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;
@@ -125,38 +126,22 @@ async fn ws_handler(ws: WebSocketUpgrade, state: State<ServerState>) -> Response
}
fn inject_address(html_bytes: Vec<u8>, address: &SocketAddr) -> Vec<u8> {
fn with_html_head<F: FnOnce(&NodeRef)>(document: &mut NodeRef, f: F) {
if let Ok(ref node) = document.select_first("head") {
f(node.as_node())
} else {
let node = NodeRef::new_element(
QualName::new(None, ns!(html), LocalName::from("head")),
None,
);
f(&node);
document.prepend(node)
}
}
let document = tauri_utils::html2::parse_doc(String::from_utf8_lossy(&html_bytes).into_owned());
let mut document = kuchiki::parse_html()
.one(String::from_utf8_lossy(&html_bytes).into_owned())
.document_node;
with_html_head(&mut document, |head| {
let script = RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli"));
let script_el = NodeRef::new_element(QualName::new(None, ns!(html), "script".into()), None);
script_el.append(NodeRef::new_text(script));
head.prepend(script_el);
});
tauri_utils::html2::append_script_to_head(
&document,
&RELOAD_SCRIPT.replace("{{reload_url}}", &format!("ws://{address}/__tauri_cli")),
);
tauri_utils::html::serialize_node(&document)
tauri_utils::html2::serialize_doc(&document)
}
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")
}
}

View 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;

View File

@@ -23,6 +23,11 @@ const ENV_TAURI_APP_PATH: &str = "TAURI_APP_PATH";
// path to the frontend app directory, usually `<project>/`
const ENV_TAURI_FRONTEND_PATH: &str = "TAURI_FRONTEND_PATH";
pub struct Dirs {
pub tauri: &'static Path,
pub frontend: &'static Path,
}
static FRONTEND_DIR: OnceLock<PathBuf> = OnceLock::new();
static TAURI_DIR: OnceLock<PathBuf> = OnceLock::new();
@@ -75,21 +80,13 @@ fn lookup<F: Fn(&PathBuf) -> bool>(dir: &Path, checker: F) -> Option<PathBuf> {
}
fn env_tauri_app_path() -> Option<PathBuf> {
std::env::var(ENV_TAURI_APP_PATH)
.map(PathBuf::from)
.ok()?
.canonicalize()
.ok()
.map(|p| dunce::simplified(&p).to_path_buf())
let p = PathBuf::from(std::env::var_os(ENV_TAURI_APP_PATH)?);
dunce::canonicalize(p).ok()
}
fn env_tauri_frontend_path() -> Option<PathBuf> {
std::env::var(ENV_TAURI_FRONTEND_PATH)
.map(PathBuf::from)
.ok()?
.canonicalize()
.ok()
.map(|p| dunce::simplified(&p).to_path_buf())
let p = PathBuf::from(std::env::var_os(ENV_TAURI_FRONTEND_PATH)?);
dunce::canonicalize(p).ok()
}
pub fn resolve_tauri_dir() -> Option<PathBuf> {
@@ -130,8 +127,8 @@ pub fn resolve_tauri_dir() -> Option<PathBuf> {
})
}
pub fn resolve() {
TAURI_DIR.set(resolve_tauri_dir().unwrap_or_else(|| {
pub fn resolve_dirs() -> Dirs {
let tauri = TAURI_DIR.get_or_init(|| resolve_tauri_dir().unwrap_or_else(|| {
let env_var_name = env_tauri_app_path().is_some().then(|| format!("`{ENV_TAURI_APP_PATH}`"));
panic!("Couldn't recognize the {} folder as a Tauri project. It must contain a `{}`, `{}` or `{}` file in any subfolder.",
env_var_name.as_deref().unwrap_or("current"),
@@ -139,16 +136,11 @@ pub fn resolve() {
ConfigFormat::Json5.into_file_name(),
ConfigFormat::Toml.into_file_name()
)
})).expect("tauri dir already resolved");
FRONTEND_DIR
.set(resolve_frontend_dir().unwrap_or_else(|| tauri_dir().parent().unwrap().to_path_buf()))
.expect("app dir already resolved");
}
pub fn tauri_dir() -> &'static PathBuf {
TAURI_DIR
.get()
.expect("app paths not initialized, this is a Tauri CLI bug")
}));
let frontend = FRONTEND_DIR.get_or_init(|| {
resolve_frontend_dir().unwrap_or_else(|| tauri.parent().unwrap().to_path_buf())
});
Dirs { tauri, frontend }
}
pub fn resolve_frontend_dir() -> Option<PathBuf> {
@@ -173,9 +165,3 @@ pub fn resolve_frontend_dir() -> Option<PathBuf> {
})
.map(|p| p.parent().unwrap().to_path_buf())
}
pub fn frontend_dir() -> &'static PathBuf {
FRONTEND_DIR
.get()
.expect("app paths not initialized, this is a Tauri CLI bug")
}

View File

@@ -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(())

View File

@@ -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(tauri_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

View File

@@ -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,15 @@ pub use tauri_utils::{config::*, platform::Target};
use std::{
collections::HashMap,
env::{current_dir, set_current_dir, set_var, var_os},
ffi::OsStr,
env::{current_dir, set_current_dir, set_var},
ffi::{OsStr, OsString},
path::Path,
process::exit,
sync::{Arc, Mutex, OnceLock},
sync::OnceLock,
};
use crate::error::Context;
pub const MERGE_CONFIG_EXTENSION_NAME: &str = "--config";
pub struct ConfigMetadata {
@@ -28,7 +31,7 @@ pub struct ConfigMetadata {
inner: Config,
/// The config extensions (platform-specific config files or the config CLI argument).
/// Maps the extension name to its value.
extensions: HashMap<String, JsonValue>,
extensions: HashMap<OsString, JsonValue>,
}
impl std::ops::Deref for ConfigMetadata {
@@ -48,12 +51,11 @@ impl ConfigMetadata {
}
/// Checks which config is overwriting the bundle identifier.
pub fn find_bundle_identifier_overwriter(&self) -> Option<String> {
pub fn find_bundle_identifier_overwriter(&self) -> Option<OsString> {
for (ext, config) in &self.extensions {
if let Some(identifier) = config
.as_object()
.and_then(|bundle_config| bundle_config.get("identifier"))
.and_then(|id| id.as_str())
.and_then(|bundle_config| bundle_config.get("identifier")?.as_str())
{
if identifier == self.inner.identifier {
return Some(ext.clone());
@@ -64,12 +66,13 @@ impl ConfigMetadata {
}
}
pub type ConfigHandle = Arc<Mutex<Option<ConfigMetadata>>>;
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_os("TAURI_BUNDLER_WIX_FIPS_COMPLIANT")
.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 +101,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"),
}
}
@@ -108,6 +110,8 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings {
header_image: config.header_image,
sidebar_image: config.sidebar_image,
installer_icon: config.installer_icon,
uninstaller_icon: config.uninstaller_icon,
uninstaller_header_image: config.uninstaller_header_image,
install_mode: config.install_mode,
languages: config.languages,
custom_language_files: config.custom_language_files,
@@ -115,6 +119,7 @@ pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings {
compression: config.compression,
start_menu_folder: config.start_menu_folder,
installer_hooks: config.installer_hooks,
#[allow(deprecated)]
minimum_webview2_version: config.minimum_webview2_version,
}
}
@@ -136,41 +141,39 @@ pub fn custom_sign_settings(
}
}
fn config_handle() -> &'static ConfigHandle {
static CONFIG_HANDLE: OnceLock<ConfigHandle> = OnceLock::new();
CONFIG_HANDLE.get_or_init(Default::default)
fn config_schema_validator() -> &'static jsonschema::Validator {
// TODO: Switch to `LazyLock` when we bump MSRV to above 1.80
static CONFIG_SCHEMA_VALIDATOR: OnceLock<jsonschema::Validator> = OnceLock::new();
CONFIG_SCHEMA_VALIDATOR.get_or_init(|| {
let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))
.expect("Failed to parse config schema bundled in the tauri-cli");
jsonschema::validator_for(&schema).expect("Config schema bundled in the tauri-cli is invalid")
})
}
/// Gets the static parsed config from `tauri.conf.json`.
fn get_internal(
fn load_config(
merge_configs: &[&serde_json::Value],
reload: bool,
target: Target,
) -> crate::Result<ConfigHandle> {
if !reload && config_handle().lock().unwrap().is_some() {
return Ok(config_handle().clone());
}
let tauri_dir = super::app_paths::tauri_dir();
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
let (mut config, config_path) =
tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))?;
let config_file_name = config_path.file_name().unwrap().to_string_lossy();
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();
let mut extensions = HashMap::new();
let original_identifier = config
.as_object()
.and_then(|config| config.get("identifier"))
.and_then(|id| id.as_str())
.and_then(|config| config.get("identifier")?.as_str())
.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(
config_path.file_name().unwrap().to_str().unwrap().into(),
platform_config,
);
extensions.insert(config_path.file_name().unwrap().into(), platform_config);
}
if !merge_configs.is_empty() {
@@ -188,16 +191,14 @@ 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 validator = jsonschema::validator_for(&schema).expect("Invalid schema");
let mut errors = validator.iter_errors(&config).peekable();
let mut errors = config_schema_validator().iter_errors(&config).peekable();
if errors.peek().is_some() {
for error in errors {
let path = error.instance_path.into_iter().join(" > ");
if path.is_empty() {
log::error!("`{}` error: {}", config_file_name, error);
log::error!("`{config_file_name:?}` error: {error}");
} else {
log::error!("`{}` error on `{}`: {}", config_file_name, path, error);
log::error!("`{config_file_name:?}` error on `{path}`: {error}");
}
}
if !reload {
@@ -208,11 +209,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 +221,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")?,
);
}
@@ -228,58 +229,54 @@ fn get_internal(
std::env::set_var(REMOVE_UNUSED_COMMANDS_ENV_VAR, tauri_dir);
}
*config_handle().lock().unwrap() = Some(ConfigMetadata {
Ok(ConfigMetadata {
target,
original_identifier,
inner: config,
extensions,
});
Ok(config_handle().clone())
})
}
pub fn get(target: Target, merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
get_internal(merge_configs, false, target)
pub fn get_config(
target: Target,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
load_config(merge_configs, false, target, tauri_dir)
}
pub fn reload(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
let target = config_handle()
.lock()
.unwrap()
.as_ref()
.map(|conf| conf.target);
if let Some(target) = target {
get_internal(merge_configs, true, target)
} else {
Err(anyhow::anyhow!("config not loaded"))
}
pub fn reload_config(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<()> {
let target = config.target;
*config = load_config(merge_configs, true, target, tauri_dir)?;
Ok(())
}
/// merges the loaded config with the given value
pub fn merge_with(merge_configs: &[&serde_json::Value]) -> crate::Result<ConfigHandle> {
let handle = config_handle();
pub fn merge_config_with(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
) -> crate::Result<()> {
if merge_configs.is_empty() {
return Ok(handle.clone());
return Ok(());
}
if let Some(config_metadata) = &mut *handle.lock().unwrap() {
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
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())?;
merge(&mut value, &merge_config);
config_metadata.inner = serde_json::from_value(value)?;
Ok(handle.clone())
} else {
Err(anyhow::anyhow!("config not loaded"))
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
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.inner.clone()).context("failed to serialize config")?;
merge(&mut value, &merge_config);
config.inner = serde_json::from_value(value).context("failed to parse config")?;
Ok(())
}
/// Same as [`json_patch::merge`] but doesn't delete the key when the patch's value is `null`

View File

@@ -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")))]

View File

@@ -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
)
}

View 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()
}

View File

@@ -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,15 +26,11 @@ use std::{
process::Command,
};
use anyhow::Context;
use tauri_utils::config::HookCommand;
use crate::{
interface::{AppInterface, Interface},
CommandExt,
};
use self::app_paths::frontend_dir;
#[cfg(not(target_os = "windows"))]
use crate::Error;
use crate::{interface::AppInterface, CommandExt};
pub fn command_env(debug: bool) -> HashMap<&'static str, String> {
let mut map = HashMap::new();
@@ -74,13 +73,14 @@ pub fn run_hook(
hook: HookCommand,
interface: &AppInterface,
debug: bool,
frontend_dir: &Path,
) -> crate::Result<()> {
let (script, script_cwd) = match hook {
HookCommand::Script(s) if s.is_empty() => (None, None),
HookCommand::Script(s) => (Some(s), None),
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
};
let cwd = script_cwd.unwrap_or_else(|| frontend_dir().clone());
let cwd = script_cwd.unwrap_or_else(|| frontend_dir.to_owned());
if let Some(script) = script {
log::info!(action = "Running"; "{} `{}`", name, script);
@@ -97,7 +97,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 +108,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 +128,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 +140,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"
)
})?;
}
}

View File

@@ -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::debug!("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::debug!("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::debug!("Failed to parse version `{version}` for NPM package `{name}`");
}
}
}
Ok(versions)
}

Some files were not shown because too many files have changed in this diff Show More