Compare commits

...

18 Commits

Author SHA1 Message Date
Fabian-Lars 51b430be98 ci: delete .changes/updater-new-bundle-support.md 2025-09-02 15:43:40 +02:00
github-actions[bot] fd439b143e Publish New Versions (v2) (#2964)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-09-02 14:04:12 +02:00
Sean Wang 2522b71f6b fix(deep-link): revert the breaking change introduced by #2928 (#2970) 2025-09-02 13:50:10 +02:00
renovate[bot] 9021a73247 chore(deps): update dependency rollup to v4.50.0 (#2966)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-31 20:53:18 +08:00
Lucas Fernandes Nogueira 625bb1c096 feat(log): re-export the log crate (#2965)
* feat(log): re-export the log crate

* code review

* Move emit_trace

---------

Co-authored-by: Tony <legendmastertony@gmail.com>
2025-08-30 23:44:03 -03:00
renovate[bot] 6215afe023 chore(deps): update dependency rollup to v4.49.0 (#2962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-29 10:40:39 +08:00
Fabian-Lars 8cf8eeab02 feat(updater): inject bundle_type into endpoint url (#2960)
* feat(updater): inject bundle_type into endpoint url

* Revert schemas

* replace with unknown if none
2025-08-27 15:52:17 -03:00
Amr Bashir 509eba8d44 feat: support message dialogs with 3 buttons (#2641)
* feat: support message dialogs with 3 buttons

* change file

* From<String>

* untagged & YesNoCancel

* revert package.json

* Update plugins/dialog/src/desktop.rs

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

* no optional

* Update desktop.rs

* Update plugins/dialog/src/models.rs

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

* change to an enum

* convert back into union

* regen

* update @since

* map buttons for linux

* enhance type

* Add examples

---------

Co-authored-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Tony <legendmastertony@gmail.com>
2025-08-27 09:40:47 -03:00
kandrelczyk 9ac5fe84e7 feat(updater): support bundle-specific targets (#2624)
* fallback targets

* linux test

* linux ready

* RPM installation

* small error fix

* fix windows build

* windows tests

* add aider files to .gitignore

* get bundle type out of patched variable

* windows tests

* patch windows binary

* format

* fix bundler

* remove local tauri dependency

* remove print

* rever Cargo.lock

* move __TAURI_BUNDLE_TYPE to tauri::utils

* get_current_bundle_type

* update tauri

* fix macos integration test

* fix fallback logic

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

* amend! fallback targets

fallback targets

* reformat

* fix tests

* reformat

* bump tari versio

* fix fallback logic

* restore Cargo.lock

* Bump tauri and add notes

* Rename some staffs

* move target logic

* Refactor the target fallback to a function

* Format and clippy

* Keep target in `Update` since it's public

* Keep updater/tests/app-updater/src/main.rs lf

* Revert changes in tests/app-updater/src/main.rs

* Clean up

* changefile

* Bump updater-js as well

* update pub fn target docs

* update pub fn target docs

* Update plugins/updater/src/error.rs

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

* Update plugins/updater/src/updater.rs

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

* Update plugins/updater/src/updater.rs

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

* suggestios

* add comment

* restore error

* Revert "Bump tauri and add notes"

This reverts commit 0a495ccc6a.

* Revert "bump tari versio"

This reverts commit 5b4c1c164b.

---------

Signed-off-by: Krzysztof Andrelczyk <cristof@curiana.net>
Co-authored-by: Lucas Nogueira <lucas@tauri.app>
Co-authored-by: Tony <legendmastertony@gmail.com>
Co-authored-by: Fabian-Lars <github@fabianlars.de>
2025-08-26 09:33:58 -03:00
renovate[bot] c247410319 chore(deps): update dependency typescript-eslint to v8.41.0 (#2956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-26 09:37:44 +08:00
Fabian-Lars de45034082 docs(store): tauri-docs compatibility 2025-08-25 20:13:16 +02:00
renovate[bot] 50c6b7c644 chore(deps): update dependency rollup to v4.48.1 (#2952)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 18:55:07 +02:00
renovate[bot] b79d02d896 chore(deps): update dependency @tauri-apps/cli to v2.8.3 (#2955)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 18:54:23 +02:00
github-actions[bot] b75f9f5cd3 publish new versions (#2954)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-08-25 18:15:22 +02:00
Lucas Fernandes Nogueira d865ed4768 fix(shell): run sidecar with dots in filename, closes #2310 (#2950)
* fix(shell): run sidecar with dots in filename, closes #2310

* fix import

* remove dead code

* code review suggestions

* clippy

* clippy
2025-08-25 10:44:47 -03:00
renovate[bot] 1107c46425 chore(deps): update dependency @tauri-apps/cli to v2.8.2 (#2932)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-25 09:25:07 +08:00
renovate[bot] 23a3705857 chore(deps): update dependency rollup to v4.48.0 (#2948)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 17:43:32 +08:00
renovate[bot] 6f65e68340 chore(deps): update eslint monorepo to v9.34.0 (#2946)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-08-23 09:05:42 +08:00
48 changed files with 1201 additions and 605 deletions
+1 -1
View File
@@ -58,4 +58,4 @@ pids
.idea
debug.log
TODO.md
.aider*
.aider.*
Generated
+12 -12
View File
@@ -207,7 +207,7 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "api"
version = "2.0.34"
version = "2.0.36"
dependencies = [
"log",
"serde",
@@ -6359,9 +6359,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.8.2"
version = "2.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a54629607ea3084a8b455c1ebe888cbdfc4de02fa5edb2e40db0dc97091007e3"
checksum = "5d545ccf7b60dcd44e07c6fb5aeb09140966f0aabd5d2aa14a6821df7bc99348"
dependencies = [
"anyhow",
"bytes",
@@ -6559,7 +6559,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.4.2"
version = "2.4.3"
dependencies = [
"dunce",
"plist",
@@ -6578,7 +6578,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-dialog"
version = "2.3.3"
version = "2.4.0"
dependencies = [
"log",
"raw-window-handle",
@@ -6691,7 +6691,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-log"
version = "2.6.0"
version = "2.7.0"
dependencies = [
"android_logger",
"byte-unit",
@@ -6818,7 +6818,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.3.0"
version = "2.3.1"
dependencies = [
"encoding_rs",
"log",
@@ -6837,7 +6837,7 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.3"
version = "2.3.4"
dependencies = [
"semver",
"serde",
@@ -7007,9 +7007,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.8.0"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bb0f10f831f75832ac74d14d98f701868f9a8adccef2c249b466cf70b607db9"
checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807"
dependencies = [
"gtk",
"http",
@@ -8789,9 +8789,9 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.53.1"
version = "0.53.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5698e50a589268aec06d2219f48b143222f7b5ad9aa690118b8dce0a8dcac574"
checksum = "e3b6763512fe4b51c80b3ce9b50939d682acb4de335dfabbdb20d7a2642199b7"
dependencies = [
"base64 0.22.1",
"block2 0.6.0",
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## \[2.0.32]
### Dependencies
- Upgraded to `dialog-js@2.4.0`
- Upgraded to `log-js@2.7.0`
## \[2.0.31]
### Dependencies
- Upgraded to `shell-js@2.3.1`
## \[2.0.30]
### Dependencies
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "api",
"private": true,
"version": "2.0.30",
"version": "2.0.32",
"type": "module",
"scripts": {
"dev": "vite --clearScreen false",
@@ -15,7 +15,7 @@
"@tauri-apps/plugin-biometric": "^2.3.0",
"@tauri-apps/plugin-cli": "^2.4.0",
"@tauri-apps/plugin-clipboard-manager": "^2.3.0",
"@tauri-apps/plugin-dialog": "^2.3.3",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-fs": "^2.4.2",
"@tauri-apps/plugin-geolocation": "^2.2.0",
"@tauri-apps/plugin-global-shortcut": "^2.3.0",
@@ -26,7 +26,7 @@
"@tauri-apps/plugin-opener": "^2.5.0",
"@tauri-apps/plugin-os": "^2.3.1",
"@tauri-apps/plugin-process": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.0",
"@tauri-apps/plugin-shell": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.0",
"@tauri-apps/plugin-updater": "^2.9.0",
"@tauri-apps/plugin-upload": "^2.3.0",
@@ -36,7 +36,7 @@
"@iconify-json/codicon": "^1.2.12",
"@iconify-json/ph": "^1.2.2",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"@tauri-apps/cli": "2.8.1",
"@tauri-apps/cli": "2.8.3",
"@unocss/extractor-svelte": "^66.3.3",
"svelte": "^5.20.4",
"unocss": "^66.3.3",
+13
View File
@@ -1,5 +1,18 @@
# Changelog
## \[2.0.36]
### Dependencies
- Upgraded to `dialog@2.4.0`
- Upgraded to `log@2.7.0`
## \[2.0.35]
### Dependencies
- Upgraded to `shell@2.3.1`
## \[2.0.34]
### Dependencies
+4 -4
View File
@@ -1,7 +1,7 @@
[package]
name = "api"
publish = false
version = "2.0.34"
version = "2.0.36"
description = "An example Tauri Application showcasing the api"
edition = "2021"
rust-version = { workspace = true }
@@ -20,12 +20,12 @@ serde = { workspace = true }
tiny_http = "0.12"
time = "0.3"
log = { workspace = true }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.6.0" }
tauri-plugin-log = { path = "../../../plugins/log", version = "2.7.0" }
tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.4.2", features = [
"watch",
] }
tauri-plugin-clipboard-manager = { path = "../../../plugins/clipboard-manager", version = "2.3.0" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.3.3" }
tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.4.0" }
tauri-plugin-http = { path = "../../../plugins/http", features = [
"multipart",
"cookies",
@@ -36,7 +36,7 @@ tauri-plugin-notification = { path = "../../../plugins/notification", version =
tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.1" }
tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.0" }
tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.0" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.0" }
tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.1" }
tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.0" }
tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" }
+21 -9
View File
@@ -44,6 +44,13 @@
await message("Tauri is awesome!");
}
async function msgCustom(result) {
const buttons = { yes: "awesome", no: "amazing", cancel: "stunning" };
await message(`Tauri is: `, { buttons })
.then((res) => onMessage(`Tauri is ${res}`))
.catch(onMessage);
}
function openDialog() {
open({
title: "My wonderful open dialog",
@@ -136,12 +143,17 @@
<label for="dialog-directory">Directory</label>
</div>
<br />
<button class="btn" id="open-dialog" on:click={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" on:click={saveDialog}
>Open save dialog</button
>
<button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" on:click={promptCustom}
>Prompt (custom)</button
>
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
<div class="flex flex-wrap flex-col md:flex-row gap-2 children:flex-shrink-0">
<button class="btn" id="open-dialog" on:click={openDialog}>Open dialog</button>
<button class="btn" id="save-dialog" on:click={saveDialog}
>Open save dialog</button
>
<button class="btn" id="prompt-dialog" on:click={prompt}>Prompt</button>
<button class="btn" id="custom-prompt-dialog" on:click={promptCustom}
>Prompt (custom)</button
>
<button class="btn" id="message-dialog" on:click={msg}>Message</button>
<button class="btn" id="message-dialog" on:click={msgCustom}>Message (custom)</button>
</div>
+4 -4
View File
@@ -11,19 +11,19 @@
"example:api:dev": "pnpm run --filter \"api\" tauri dev"
},
"devDependencies": {
"@eslint/js": "9.33.0",
"@eslint/js": "9.34.0",
"@rollup/plugin-node-resolve": "16.0.1",
"@rollup/plugin-terser": "0.4.4",
"@rollup/plugin-typescript": "12.1.4",
"covector": "^0.12.4",
"eslint": "9.33.0",
"eslint": "9.34.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-security": "3.0.1",
"prettier": "3.6.2",
"rollup": "4.47.1",
"rollup": "4.50.0",
"tslib": "2.8.1",
"typescript": "5.9.2",
"typescript-eslint": "8.40.0"
"typescript-eslint": "8.41.0"
},
"pnpm": {
"overrides": {
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.4.3]
- [`2522b71f`](https://github.com/tauri-apps/plugins-workspace/commit/2522b71f6bcae65c03b24415eb9295c9e7c84ffc) ([#2970](https://github.com/tauri-apps/plugins-workspace/pull/2970) by [@WSH032](https://github.com/tauri-apps/plugins-workspace/../../WSH032)) Revert the breaking change introduced by [#2928](https://github.com/tauri-apps/plugins-workspace/pull/2928).
## \[2.4.2]
- [`21d721a0`](https://github.com/tauri-apps/plugins-workspace/commit/21d721a0c2731fc201872f5b99ea8bbdc61b0b60) ([#2928](https://github.com/tauri-apps/plugins-workspace/pull/2928) by [@FabianLars](https://github.com/tauri-apps/plugins-workspace/../../FabianLars)) On Linux, improved error messages when OS commands fail.
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-deep-link"
version = "2.4.2"
version = "2.4.3"
description = "Set your Tauri application as the default handler for an URL"
authors = { workspace = true }
license = { workspace = true }
@@ -1,5 +1,11 @@
# Changelog
## \[2.2.6]
### Dependencies
- Upgraded to `deep-link-js@2.4.3`
## \[2.2.5]
### Dependencies
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "deep-link-example",
"private": true,
"version": "2.2.5",
"version": "2.2.6",
"type": "module",
"scripts": {
"dev": "vite",
@@ -11,10 +11,10 @@
},
"dependencies": {
"@tauri-apps/api": "2.8.0",
"@tauri-apps/plugin-deep-link": "2.4.2"
"@tauri-apps/plugin-deep-link": "2.4.3"
},
"devDependencies": {
"@tauri-apps/cli": "2.8.1",
"@tauri-apps/cli": "2.8.3",
"typescript": "^5.7.3",
"vite": "^7.0.4"
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-deep-link",
"version": "2.4.2",
"version": "2.4.3",
"description": "Set your Tauri application as the default handler for an URL",
"license": "MIT OR Apache-2.0",
"authors": [
@@ -28,6 +28,6 @@
"@tauri-apps/api": "^2.8.0"
},
"devDependencies": {
"@tauri-apps/cli": "2.8.1"
"@tauri-apps/cli": "2.8.3"
}
}
+10 -3
View File
@@ -23,14 +23,21 @@ pub enum Error {
#[cfg(target_os = "linux")]
#[error(transparent)]
ParseIni(#[from] ini::ParseError),
#[cfg(target_os = "linux")]
#[error("Failed to run OS command `{0}`: {1}")]
Execute(&'static str, #[source] std::io::Error),
#[cfg(mobile)]
#[error(transparent)]
PluginInvoke(#[from] tauri::plugin::mobile::PluginInvokeError),
}
// TODO(v3): change this into an error in v3,
// see <https://github.com/tauri-apps/plugins-workspace/pull/2970#issuecomment-3244660138>.
#[inline]
#[cfg(target_os = "linux")]
pub(crate) fn inspect_command_error<'a>(command: &'a str) -> impl Fn(&std::io::Error) + 'a {
move |e| {
tracing::error!("Failed to run OS command `{command}`: {e}");
}
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
+5 -3
View File
@@ -334,12 +334,14 @@ mod imp {
Command::new("update-desktop-database")
.arg(target)
.status()
.map_err(|error| crate::Error::Execute("update-desktop-database", error))?;
.inspect_err(crate::error::inspect_command_error(
"update-desktop-database",
))?;
Command::new("xdg-mime")
.args(["default", &file_name, mime_type.as_str()])
.status()
.map_err(|error| crate::Error::Execute("xdg-mime", error))?;
.inspect_err(crate::error::inspect_command_error("xdg-mime"))?;
Ok(())
}
@@ -444,7 +446,7 @@ mod imp {
&format!("x-scheme-handler/{}", _protocol.as_ref()),
])
.output()
.map_err(|error| crate::Error::Execute("xdg-mime", error))?;
.inspect_err(crate::error::inspect_command_error("xdg-mime"))?;
Ok(String::from_utf8_lossy(&output.stdout).contains(&file_name))
}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.4.0]
- [`509eba8d`](https://github.com/tauri-apps/plugins-workspace/commit/509eba8d441c4f6ecf0af77b572cb2afd69a752d) ([#2641](https://github.com/tauri-apps/plugins-workspace/pull/2641) by [@amrbashir](https://github.com/tauri-apps/plugins-workspace/../../amrbashir)) Add support for showing a message dialog with 3 buttons.
## \[2.3.3]
### Dependencies
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-dialog"
version = "2.3.3"
version = "2.4.0"
description = "Native system dialogs for opening and saving files along with message dialogs on your Tauri application."
edition = { workspace = true }
authors = { workspace = true }
@@ -38,6 +38,7 @@ class MessageOptions {
var title: String? = null
lateinit var message: String
var okButtonLabel: String? = null
var noButtonLabel: String? = null
var cancelButtonLabel: String? = null
}
@@ -139,9 +140,8 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
return
}
val handler = { cancelled: Boolean, value: Boolean ->
val handler = { value: String ->
val ret = JSObject()
ret.put("cancelled", cancelled)
ret.put("value", value)
invoke.resolve(ret)
}
@@ -153,24 +153,34 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
if (args.title != null) {
builder.setTitle(args.title)
}
val okButtonLabel = args.okButtonLabel ?: "Ok"
builder
.setMessage(args.message)
.setPositiveButton(
args.okButtonLabel ?: "OK"
) { dialog, _ ->
.setPositiveButton(okButtonLabel) { dialog, _ ->
dialog.dismiss()
handler(false, true)
handler(okButtonLabel)
}
.setOnCancelListener { dialog ->
dialog.dismiss()
handler(true, false)
handler(args.cancelButtonLabel ?: "Cancel")
}
if (args.noButtonLabel != null) {
builder.setNeutralButton(args.noButtonLabel) { dialog, _ ->
dialog.dismiss()
handler(args.noButtonLabel!!)
}
}
if (args.cancelButtonLabel != null) {
builder.setNegativeButton( args.cancelButtonLabel) { dialog, _ ->
dialog.dismiss()
handler(false, false)
handler(args.cancelButtonLabel!!)
}
}
val dialog = builder.create()
dialog.show()
}
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,yesButtonLabel:i?.okLabel?.toString(),noButtonLabel:i?.cancelLabel?.toString()})},t.confirm=async function(t,e){const i="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),cancelButtonLabel:i?.cancelLabel?.toString()})},t.message=async function(t,e){const i="string"==typeof e?{title:e}:e;await n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString()})},t.open=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|open",{options:t})},t.save=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|save",{options:t})},t}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_DIALOG__=function(t){"use strict";async function n(t,n={},e){return window.__TAURI_INTERNALS__.invoke(t,n,e)}function e(t){if(void 0!==t)return"string"==typeof t?t:"ok"in t&&"cancel"in t?{OkCancelCustom:[t.ok,t.cancel]}:"yes"in t&&"no"in t&&"cancel"in t?{YesNoCancelCustom:[t.yes,t.no,t.cancel]}:"ok"in t?{OkCustom:t.ok}:void 0}return"function"==typeof SuppressedError&&SuppressedError,t.ask=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|ask",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,yesButtonLabel:o?.okLabel?.toString(),noButtonLabel:o?.cancelLabel?.toString()})},t.confirm=async function(t,e){const o="string"==typeof e?{title:e}:e;return await n("plugin:dialog|confirm",{message:t.toString(),title:o?.title?.toString(),kind:o?.kind,okButtonLabel:o?.okLabel?.toString(),cancelButtonLabel:o?.cancelLabel?.toString()})},t.message=async function(t,o){const i="string"==typeof o?{title:o}:o;return n("plugin:dialog|message",{message:t.toString(),title:i?.title?.toString(),kind:i?.kind,okButtonLabel:i?.okLabel?.toString(),buttons:e(i?.buttons)})},t.open=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|open",{options:t})},t.save=async function(t={}){return"object"==typeof t&&Object.freeze(t),await n("plugin:dialog|save",{options:t})},t}({});Object.defineProperty(window.__TAURI__,"dialog",{value:__TAURI_PLUGIN_DIALOG__})}
+140 -4
View File
@@ -77,6 +77,80 @@ interface SaveDialogOptions {
canCreateDirectories?: boolean
}
/**
* Default buttons for a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogDefaultButtons =
| 'Ok'
| 'OkCancel'
| 'YesNo'
| 'YesNoCancel'
/** All possible button keys. */
type ButtonKey = 'ok' | 'cancel' | 'yes' | 'no'
/** Ban everything except a set of keys. */
type BanExcept<Allowed extends ButtonKey> = Partial<
Record<Exclude<ButtonKey, Allowed>, never>
>
/**
* The Yes, No and Cancel buttons of a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogButtonsYesNoCancel = {
/** The Yes button. */
yes: string
/** The No button. */
no: string
/** The Cancel button. */
cancel: string
} & BanExcept<'yes' | 'no' | 'cancel'>
/**
* The Ok and Cancel buttons of a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogButtonsOkCancel = {
/** The Ok button. */
ok: string
/** The Cancel button. */
cancel: string
} & BanExcept<'ok' | 'cancel'>
/**
* The Ok button of a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogButtonsOk = {
/** The Ok button. */
ok: string
} & BanExcept<'ok'>
/**
* Custom buttons for a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogCustomButtons =
| MessageDialogButtonsYesNoCancel
| MessageDialogButtonsOkCancel
| MessageDialogButtonsOk
/**
* The buttons of a message dialog.
*
* @since 2.4.0
*/
export type MessageDialogButtons =
| MessageDialogDefaultButtons
| MessageDialogCustomButtons
/**
* @since 2.0.0
*/
@@ -85,8 +159,58 @@ interface MessageDialogOptions {
title?: string
/** The kind of the dialog. Defaults to `info`. */
kind?: 'info' | 'warning' | 'error'
/** The label of the confirm button. */
/**
* The label of the Ok button.
*
* @deprecated Use {@linkcode MessageDialogOptions.buttons} instead.
*/
okLabel?: string
/**
* The buttons of the dialog.
*
* @example
*
* ```ts
* // Use system default buttons texts
* await message('Hello World!', { buttons: 'Ok' })
* await message('Hello World!', { buttons: 'OkCancel' })
*
* // Or with custom button texts
* await message('Hello World!', { buttons: { ok: 'Yes!' } })
* await message('Take on the task?', {
* buttons: { ok: 'Accept', cancel: 'Cancel' }
* })
* await message('Show the file content?', {
* buttons: { yes: 'Show content', no: 'Show in folder', cancel: 'Cancel' }
* })
* ```
*
* @since 2.4.0
*/
buttons?: MessageDialogButtons
}
/**
* Internal function to convert the buttons to the Rust type.
*/
function buttonsToRust(buttons: MessageDialogButtons | undefined) {
if (buttons === undefined) {
return undefined
}
if (typeof buttons === 'string') {
return buttons
} else if ('ok' in buttons && 'cancel' in buttons) {
return { OkCancelCustom: [buttons.ok, buttons.cancel] }
} else if ('yes' in buttons && 'no' in buttons && 'cancel' in buttons) {
return {
YesNoCancelCustom: [buttons.yes, buttons.no, buttons.cancel]
}
} else if ('ok' in buttons) {
return { OkCustom: buttons.ok }
}
return undefined
}
interface ConfirmDialogOptions {
@@ -202,6 +326,16 @@ async function save(options: SaveDialogOptions = {}): Promise<string | null> {
return await invoke('plugin:dialog|save', { options })
}
/**
* The result of a message dialog.
*
* The result is a string if the dialog has custom buttons,
* otherwise it is one of the default buttons.
*
* @since 2.4.0
*/
export type MessageDialogResult = 'Yes' | 'No' | 'Ok' | 'Cancel' | (string & {})
/**
* Shows a message dialog with an `Ok` button.
* @example
@@ -222,13 +356,15 @@ async function save(options: SaveDialogOptions = {}): Promise<string | null> {
async function message(
message: string,
options?: string | MessageDialogOptions
): Promise<void> {
): Promise<MessageDialogResult> {
const opts = typeof options === 'string' ? { title: options } : options
await invoke('plugin:dialog|message', {
return invoke<MessageDialogResult>('plugin:dialog|message', {
message: message.toString(),
title: opts?.title?.toString(),
kind: opts?.kind,
okButtonLabel: opts?.okLabel?.toString()
okButtonLabel: opts?.okLabel?.toString(),
buttons: buttonsToRust(opts?.buttons)
})
}
+22 -19
View File
@@ -20,6 +20,7 @@ struct MessageDialogOptions: Decodable {
var title: String?
let message: String
var okButtonLabel: String?
var noButtonLabel: String?
var cancelButtonLabel: String?
}
@@ -200,36 +201,38 @@ class DialogPlugin: Plugin {
let alert = UIAlertController(
title: args.title, message: args.message, preferredStyle: UIAlertController.Style.alert)
let cancelButtonLabel = args.cancelButtonLabel ?? ""
if !cancelButtonLabel.isEmpty {
if let cancelButtonLabel = args.cancelButtonLabel {
alert.addAction(
UIAlertAction(
title: cancelButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("cancel")
invoke.resolve([
"value": false,
"cancelled": false,
])
}))
invoke.resolve(["value": cancelButtonLabel])
}
)
)
}
let okButtonLabel = args.okButtonLabel ?? (cancelButtonLabel.isEmpty ? "OK" : "")
if !okButtonLabel.isEmpty {
if let noButtonLabel = args.noButtonLabel {
alert.addAction(
UIAlertAction(
title: okButtonLabel, style: UIAlertAction.Style.default,
title: noButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
Logger.error("ok")
invoke.resolve([
"value": true,
"cancelled": false,
])
}))
invoke.resolve(["value": noButtonLabel])
}
)
)
}
let okButtonLabel = args.okButtonLabel ?? "Ok"
alert.addAction(
UIAlertAction(
title: okButtonLabel, style: UIAlertAction.Style.default,
handler: { (_) -> Void in
invoke.resolve(["value": okButtonLabel])
}
)
)
manager.viewController?.present(alert, animated: true, completion: nil)
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-dialog",
"version": "2.3.3",
"version": "2.4.0",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
+21 -21
View File
@@ -9,8 +9,8 @@ use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{
Dialog, FileDialogBuilder, FilePath, MessageDialogButtons, MessageDialogKind, Result, CANCEL,
NO, OK, YES,
Dialog, FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogButtons,
MessageDialogKind, MessageDialogResult, Result, CANCEL, NO, OK, YES,
};
#[derive(Serialize)]
@@ -248,7 +248,7 @@ fn message_dialog<R: Runtime>(
message: String,
kind: Option<MessageDialogKind>,
buttons: MessageDialogButtons,
) -> bool {
) -> MessageDialogBuilder<R> {
let mut builder = dialog.message(message);
builder = builder.buttons(buttons);
@@ -266,7 +266,7 @@ fn message_dialog<R: Runtime>(
builder = builder.kind(kind);
}
builder.blocking_show()
builder
}
#[command]
@@ -277,19 +277,15 @@ pub(crate) async fn message<R: Runtime>(
message: String,
kind: Option<MessageDialogKind>,
ok_button_label: Option<String>,
) -> Result<bool> {
Ok(message_dialog(
window,
dialog,
title,
message,
kind,
if let Some(ok_button_label) = ok_button_label {
MessageDialogButtons::OkCustom(ok_button_label)
} else {
MessageDialogButtons::Ok
},
))
buttons: Option<MessageDialogButtons>,
) -> Result<MessageDialogResult> {
let buttons = buttons.unwrap_or(if let Some(ok_button_label) = ok_button_label {
MessageDialogButtons::OkCustom(ok_button_label)
} else {
MessageDialogButtons::Ok
});
Ok(message_dialog(window, dialog, title, message, kind, buttons).blocking_show_with_result())
}
#[command]
@@ -302,7 +298,7 @@ pub(crate) async fn ask<R: Runtime>(
yes_button_label: Option<String>,
no_button_label: Option<String>,
) -> Result<bool> {
Ok(message_dialog(
let dialog = message_dialog(
window,
dialog,
title,
@@ -318,7 +314,9 @@ pub(crate) async fn ask<R: Runtime>(
} else {
MessageDialogButtons::YesNo
},
))
);
Ok(dialog.blocking_show())
}
#[command]
@@ -331,7 +329,7 @@ pub(crate) async fn confirm<R: Runtime>(
ok_button_label: Option<String>,
cancel_button_label: Option<String>,
) -> Result<bool> {
Ok(message_dialog(
let dialog = message_dialog(
window,
dialog,
title,
@@ -347,5 +345,7 @@ pub(crate) async fn confirm<R: Runtime>(
} else {
MessageDialogButtons::OkCancel
},
))
);
Ok(dialog.blocking_show())
}
+40 -18
View File
@@ -13,7 +13,7 @@ use rfd::{AsyncFileDialog, AsyncMessageDialog};
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder, OK};
use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder};
pub fn init<R: Runtime, C: DeserializeOwned>(
app: &AppHandle<R>,
@@ -115,6 +115,10 @@ impl From<MessageDialogButtons> for rfd::MessageButtons {
MessageDialogButtons::YesNo => Self::YesNo,
MessageDialogButtons::OkCustom(ok) => Self::OkCustom(ok),
MessageDialogButtons::OkCancelCustom(ok, cancel) => Self::OkCancelCustom(ok, cancel),
MessageDialogButtons::YesNoCancel => Self::YesNoCancel,
MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
Self::YesNoCancelCustom(yes, no, cancel)
}
}
}
}
@@ -208,28 +212,46 @@ pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
}
/// Shows a message dialog
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
pub fn show_message_dialog<R: Runtime, F: FnOnce(MessageDialogResult) + Send + 'static>(
dialog: MessageDialogBuilder<R>,
f: F,
callback: F,
) {
use rfd::MessageDialogResult;
let ok_label = match &dialog.buttons {
MessageDialogButtons::OkCustom(ok) => Some(ok.clone()),
MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()),
_ => None,
};
let f = move |res| {
f(match res {
MessageDialogResult::Ok | MessageDialogResult::Yes => true,
MessageDialogResult::Custom(s) => ok_label.map_or(s == OK, |ok_label| ok_label == s),
_ => false,
});
};
let f = move |res: rfd::MessageDialogResult| callback(res.into());
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
let buttons = dialog.buttons.clone();
let dialog = AsyncMessageDialog::from(dialog).show();
std::thread::spawn(move || f(tauri::async_runtime::block_on(dialog)));
std::thread::spawn(move || {
let result = tauri::async_runtime::block_on(dialog);
// on Linux rfd does not return rfd::MessageDialogResult::Custom, so we must map manually
let result = match (result, buttons) {
(rfd::MessageDialogResult::Ok, MessageDialogButtons::OkCustom(s)) => {
rfd::MessageDialogResult::Custom(s)
}
(
rfd::MessageDialogResult::Ok,
MessageDialogButtons::OkCancelCustom(ok, _cancel),
) => rfd::MessageDialogResult::Custom(ok),
(
rfd::MessageDialogResult::Cancel,
MessageDialogButtons::OkCancelCustom(_ok, cancel),
) => rfd::MessageDialogResult::Custom(cancel),
(
rfd::MessageDialogResult::Yes,
MessageDialogButtons::YesNoCancelCustom(yes, _no, _cancel),
) => rfd::MessageDialogResult::Custom(yes),
(
rfd::MessageDialogResult::No,
MessageDialogButtons::YesNoCancelCustom(_yes, no, _cancel),
) => rfd::MessageDialogResult::Custom(no),
(
rfd::MessageDialogResult::Cancel,
MessageDialogButtons::YesNoCancelCustom(_yes, _no, cancel),
) => rfd::MessageDialogResult::Custom(cancel),
(result, _) => result,
};
f(result);
});
});
}
+51 -6
View File
@@ -216,6 +216,7 @@ pub(crate) struct MessageDialogPayload<'a> {
message: &'a String,
kind: &'a MessageDialogKind,
ok_button_label: Option<&'a str>,
no_button_label: Option<&'a str>,
cancel_button_label: Option<&'a str>,
}
@@ -238,13 +239,17 @@ impl<R: Runtime> MessageDialogBuilder<R> {
#[cfg(mobile)]
pub(crate) fn payload(&self) -> MessageDialogPayload<'_> {
let (ok_button_label, cancel_button_label) = match &self.buttons {
MessageDialogButtons::Ok => (Some(OK), None),
MessageDialogButtons::OkCancel => (Some(OK), Some(CANCEL)),
MessageDialogButtons::YesNo => (Some(YES), Some(NO)),
MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), Some(CANCEL)),
let (ok_button_label, no_button_label, cancel_button_label) = match &self.buttons {
MessageDialogButtons::Ok => (Some(OK), None, None),
MessageDialogButtons::OkCancel => (Some(OK), None, Some(CANCEL)),
MessageDialogButtons::YesNo => (Some(YES), Some(NO), None),
MessageDialogButtons::YesNoCancel => (Some(YES), Some(NO), Some(CANCEL)),
MessageDialogButtons::OkCustom(ok) => (Some(ok.as_str()), None, None),
MessageDialogButtons::OkCancelCustom(ok, cancel) => {
(Some(ok.as_str()), Some(cancel.as_str()))
(Some(ok.as_str()), None, Some(cancel.as_str()))
}
MessageDialogButtons::YesNoCancelCustom(yes, no, cancel) => {
(Some(yes.as_str()), Some(no.as_str()), Some(cancel.as_str()))
}
};
MessageDialogPayload {
@@ -252,6 +257,7 @@ impl<R: Runtime> MessageDialogBuilder<R> {
message: &self.message,
kind: &self.kind,
ok_button_label,
no_button_label,
cancel_button_label,
}
}
@@ -295,16 +301,55 @@ impl<R: Runtime> MessageDialogBuilder<R> {
}
/// Shows a message dialog
///
/// Returns `true` if the user pressed the OK/Yes button,
pub fn show<F: FnOnce(bool) + Send + 'static>(self, f: F) {
let ok_label = match &self.buttons {
MessageDialogButtons::OkCustom(ok) => Some(ok.clone()),
MessageDialogButtons::OkCancelCustom(ok, _) => Some(ok.clone()),
MessageDialogButtons::YesNoCancelCustom(yes, _, _) => Some(yes.clone()),
_ => None,
};
show_message_dialog(self, move |res| {
let sucess = match res {
MessageDialogResult::Ok | MessageDialogResult::Yes => true,
MessageDialogResult::Custom(s) => {
ok_label.map_or(s == OK, |ok_label| ok_label == s)
}
_ => false,
};
f(sucess)
})
}
/// Shows a message dialog and returns the button that was pressed.
///
/// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
pub fn show_with_result<F: FnOnce(MessageDialogResult) + Send + 'static>(self, f: F) {
show_message_dialog(self, f)
}
/// Shows a message dialog.
///
/// Returns `true` if the user pressed the OK/Yes button,
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
pub fn blocking_show(self) -> bool {
blocking_fn!(self, show)
}
/// Shows a message dialog and returns the button that was pressed.
///
/// Returns a [`MessageDialogResult`] enum that indicates which button was pressed.
///
/// This is a blocking operation,
/// and should *NOT* be used when running on the main thread context.
pub fn blocking_show_with_result(self) -> MessageDialogResult {
blocking_fn!(self, show_with_result)
}
}
#[derive(Debug, Serialize)]
pub(crate) struct Filter {
+6 -6
View File
@@ -8,7 +8,7 @@ use tauri::{
AppHandle, Runtime,
};
use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder};
use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder, MessageDialogResult};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
@@ -107,13 +107,11 @@ pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
#[derive(Debug, Deserialize)]
struct ShowMessageDialogResponse {
#[allow(dead_code)]
cancelled: bool,
value: bool,
value: String,
}
/// Shows a message dialog
pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
pub fn show_message_dialog<R: Runtime, F: FnOnce(MessageDialogResult) + Send + 'static>(
dialog: MessageDialogBuilder<R>,
f: F,
) {
@@ -122,6 +120,8 @@ pub fn show_message_dialog<R: Runtime, F: FnOnce(bool) + Send + 'static>(
.dialog
.0
.run_mobile_plugin::<ShowMessageDialogResponse>("showMessageDialog", dialog.payload());
f(res.map(|r| r.value).unwrap_or_default())
let res = res.map(|res| res.value.into());
f(res.unwrap_or_default())
});
}
+42 -1
View File
@@ -52,7 +52,7 @@ impl Serialize for MessageDialogKind {
/// Set of button that will be displayed on the dialog
#[non_exhaustive]
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub enum MessageDialogButtons {
#[default]
/// A single `Ok` button with OS default dialog text
@@ -61,8 +61,49 @@ pub enum MessageDialogButtons {
OkCancel,
/// 2 buttons `Yes` and `No` with OS default dialog texts
YesNo,
/// 3 buttons `Yes`, `No` and `Cancel` with OS default dialog texts
YesNoCancel,
/// A single `Ok` button with custom text
OkCustom(String),
/// 2 buttons `Ok` and `Cancel` with custom texts
OkCancelCustom(String, String),
/// 3 buttons `Yes`, `No` and `Cancel` with custom texts
YesNoCancelCustom(String, String, String),
}
/// Result of a message dialog
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum MessageDialogResult {
Yes,
No,
Ok,
#[default]
Cancel,
#[serde(untagged)]
Custom(String),
}
#[cfg(desktop)]
impl From<rfd::MessageDialogResult> for MessageDialogResult {
fn from(result: rfd::MessageDialogResult) -> Self {
match result {
rfd::MessageDialogResult::Yes => Self::Yes,
rfd::MessageDialogResult::No => Self::No,
rfd::MessageDialogResult::Ok => Self::Ok,
rfd::MessageDialogResult::Cancel => Self::Cancel,
rfd::MessageDialogResult::Custom(s) => Self::Custom(s),
}
}
}
impl From<String> for MessageDialogResult {
fn from(value: String) -> Self {
match value.as_str() {
"Yes" => Self::Yes,
"No" => Self::No,
"Ok" => Self::Ok,
"Cancel" => Self::Cancel,
_ => Self::Custom(value),
}
}
}
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.7.0]
- [`625bb1c0`](https://github.com/tauri-apps/plugins-workspace/commit/625bb1c0965394b88522643731f78ccbcca84add) ([#2965](https://github.com/tauri-apps/plugins-workspace/pull/2965) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Re-export the log crate.
## \[2.6.0]
- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-log"
version = "2.6.0"
version = "2.7.0"
description = "Configurable logging for your Tauri app."
authors = { workspace = true }
license = { workspace = true }
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-log",
"version": "2.6.0",
"version": "2.7.0",
"description": "Configurable logging for your Tauri app.",
"license": "MIT OR Apache-2.0",
"authors": [
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::collections::HashMap;
use log::RecordBuilder;
use crate::{LogLevel, WEBVIEW_TARGET};
#[tauri::command]
pub fn log(
level: LogLevel,
message: String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
key_values: Option<HashMap<String, String>>,
) {
let level = log::Level::from(level);
let target = if let Some(location) = location {
format!("{WEBVIEW_TARGET}:{location}")
} else {
WEBVIEW_TARGET.to_string()
};
let mut builder = RecordBuilder::new();
builder.level(level).target(&target).file(file).line(line);
let key_values = key_values.unwrap_or_default();
let mut kv = HashMap::new();
for (k, v) in key_values.iter() {
kv.insert(k.as_str(), v.as_str());
}
builder.key_values(&kv);
#[cfg(feature = "tracing")]
emit_trace(level, &message, location, file, line, &kv);
log::logger().log(&builder.args(format_args!("{message}")).build());
}
// Target becomes default and location is added as a parameter
#[cfg(feature = "tracing")]
fn emit_trace(
level: log::Level,
message: &String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
kv: &HashMap<&str, &str>,
) {
macro_rules! emit_event {
($level:expr) => {
tracing::event!(
target: WEBVIEW_TARGET,
$level,
message = %message,
location = location,
file,
line,
?kv
)
};
}
match level {
log::Level::Error => emit_event!(tracing::Level::ERROR),
log::Level::Warn => emit_event!(tracing::Level::WARN),
log::Level::Info => emit_event!(tracing::Level::INFO),
log::Level::Debug => emit_event!(tracing::Level::DEBUG),
log::Level::Trace => emit_event!(tracing::Level::TRACE),
}
}
+4 -67
View File
@@ -10,12 +10,10 @@
)]
use fern::{Filter, FormatCallback};
use log::{logger, RecordBuilder};
use log::{LevelFilter, Record};
use serde::Serialize;
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::borrow::Cow;
use std::collections::HashMap;
use std::{
fmt::Arguments,
fs::{self, File},
@@ -30,6 +28,9 @@ use tauri::{AppHandle, Emitter};
use time::{macros::format_description, OffsetDateTime};
pub use fern;
pub use log;
mod commands;
pub const WEBVIEW_TARGET: &str = "webview";
@@ -206,70 +207,6 @@ impl Target {
}
}
// Target becomes default and location is added as a parameter
#[cfg(feature = "tracing")]
fn emit_trace(
level: log::Level,
message: &String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
kv: &HashMap<&str, &str>,
) {
macro_rules! emit_event {
($level:expr) => {
tracing::event!(
target: WEBVIEW_TARGET,
$level,
message = %message,
location = location,
file,
line,
?kv
)
};
}
match level {
log::Level::Error => emit_event!(tracing::Level::ERROR),
log::Level::Warn => emit_event!(tracing::Level::WARN),
log::Level::Info => emit_event!(tracing::Level::INFO),
log::Level::Debug => emit_event!(tracing::Level::DEBUG),
log::Level::Trace => emit_event!(tracing::Level::TRACE),
}
}
#[tauri::command]
fn log(
level: LogLevel,
message: String,
location: Option<&str>,
file: Option<&str>,
line: Option<u32>,
key_values: Option<HashMap<String, String>>,
) {
let level = log::Level::from(level);
let target = if let Some(location) = location {
format!("{WEBVIEW_TARGET}:{location}")
} else {
WEBVIEW_TARGET.to_string()
};
let mut builder = RecordBuilder::new();
builder.level(level).target(&target).file(file).line(line);
let key_values = key_values.unwrap_or_default();
let mut kv = HashMap::new();
for (k, v) in key_values.iter() {
kv.insert(k.as_str(), v.as_str());
}
builder.key_values(&kv);
#[cfg(feature = "tracing")]
emit_trace(level, &message, location, file, line, &kv);
logger().log(&builder.args(format_args!("{message}")).build());
}
pub struct Builder {
dispatch: fern::Dispatch,
rotation_strategy: RotationStrategy,
@@ -528,7 +465,7 @@ impl Builder {
}
fn plugin_builder<R: Runtime>() -> plugin::Builder<R> {
plugin::Builder::new("log").invoke_handler(tauri::generate_handler![log])
plugin::Builder::new("log").invoke_handler(tauri::generate_handler![commands::log])
}
#[allow(clippy::type_complexity)]
+4
View File
@@ -1,5 +1,9 @@
# Changelog
## \[2.3.1]
- [`d865ed47`](https://github.com/tauri-apps/plugins-workspace/commit/d865ed47685c3923e894f7d10ee4c037507037e6) ([#2950](https://github.com/tauri-apps/plugins-workspace/pull/2950) by [@lucasfernog](https://github.com/tauri-apps/plugins-workspace/../../lucasfernog)) Fix sidecar with dots in the filename not working on Windows.
## \[2.3.0]
- [`f209b2f2`](https://github.com/tauri-apps/plugins-workspace/commit/f209b2f23cb29133c97ad5961fb46ef794dbe063) ([#2804](https://github.com/tauri-apps/plugins-workspace/pull/2804) by [@renovate](https://github.com/tauri-apps/plugins-workspace/../../renovate)) Updated tauri to 2.6
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-shell"
version = "2.3.0"
version = "2.3.1"
description = "Access the system shell. Allows you to spawn child processes and manage files and URLs using their default application."
edition = { workspace = true }
authors = { workspace = true }
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-shell",
"version": "2.3.0",
"version": "2.3.1",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
+6 -1
View File
@@ -115,7 +115,12 @@ fn prepare_cmd<R: Runtime>(
let mut command = if options.sidecar {
let program = PathBuf::from(program);
let program_as_string = program.display().to_string();
let program_no_ext_as_string = program.with_extension("").display().to_string();
let has_extension = program.extension().is_some_and(|ext| ext == "exe");
let program_no_ext_as_string = if has_extension {
program.with_extension("").display().to_string()
} else {
program_as_string.clone()
};
let configured_sidecar = window
.config()
.bundle
+45 -3
View File
@@ -118,9 +118,23 @@ pub struct Output {
fn relative_command_path(command: &Path) -> crate::Result<PathBuf> {
match platform::current_exe()?.parent() {
#[cfg(windows)]
Some(exe_dir) => Ok(exe_dir.join(command).with_extension("exe")),
Some(exe_dir) => {
let mut command_path = exe_dir.join(command);
let already_exe = command_path.extension().is_some_and(|ext| ext == "exe");
if !already_exe {
// do not use with_extension to retain dots in the command filename
command_path.as_mut_os_string().push(".exe");
}
Ok(command_path)
}
#[cfg(not(windows))]
Some(exe_dir) => Ok(exe_dir.join(command)),
Some(exe_dir) => {
let mut command_path = exe_dir.join(command);
if command_path.extension().is_some_and(|ext| ext == "exe") {
command_path.set_extension("");
}
Ok(command_path)
}
None => Err(crate::Error::CurrentExeHasNoParent),
}
}
@@ -133,6 +147,10 @@ impl From<Command> for StdCommand {
impl Command {
pub(crate) fn new<S: AsRef<OsStr>>(program: S) -> Self {
log::debug!(
"Creating sidecar {}",
program.as_ref().to_str().unwrap_or("")
);
let mut command = StdCommand::new(program);
command.stdout(Stdio::piped());
@@ -451,9 +469,33 @@ fn spawn_pipe_reader<F: Fn(Vec<u8>) -> CommandEvent + Send + Copy + 'static>(
// tests for the commands functions.
#[cfg(test)]
mod tests {
#[cfg(not(windows))]
use super::*;
#[test]
fn relative_command_path_resolves() {
let cwd_parent = platform::current_exe()
.unwrap()
.parent()
.unwrap()
.to_owned();
assert_eq!(
relative_command_path(Path::new("Tauri.Example")).unwrap(),
cwd_parent.join(if cfg!(windows) {
"Tauri.Example.exe"
} else {
"Tauri.Example"
})
);
assert_eq!(
relative_command_path(Path::new("Tauri.Example.exe")).unwrap(),
cwd_parent.join(if cfg!(windows) {
"Tauri.Example.exe"
} else {
"Tauri.Example"
})
);
}
#[cfg(not(windows))]
#[test]
fn test_cmd_spawn_output() {
+6
View File
@@ -1,5 +1,11 @@
# Changelog
## \[2.3.4]
### Dependencies
- Upgraded to `deep-link@2.4.3`
## \[2.3.3]
### Dependencies
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-single-instance"
version = "2.3.3"
version = "2.3.4"
description = "Ensure a single instance of your tauri app is running."
authors = { workspace = true }
license = { workspace = true }
@@ -26,7 +26,7 @@ serde_json = { workspace = true }
tauri = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.2", optional = true }
tauri-plugin-deep-link = { path = "../deep-link", version = "2.4.3", optional = true }
semver = { version = "1", optional = true }
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
@@ -9,6 +9,6 @@
"author": "",
"license": "MIT",
"devDependencies": {
"@tauri-apps/cli": "2.8.1"
"@tauri-apps/cli": "2.8.3"
}
}
@@ -8,7 +8,7 @@
"tauri": "tauri"
},
"devDependencies": {
"@tauri-apps/cli": "2.8.1",
"@tauri-apps/cli": "2.8.3",
"typescript": "^5.7.3",
"vite": "^7.0.4"
}
+1 -1
View File
@@ -407,7 +407,7 @@ interface IStore {
* Note:
* - This method loads the data and merges it with the current store,
* this behavior will be changed to resetting to default first and then merging with the on-disk state in v3,
* to fully match the store with the on-disk state, set {@linkcode ReloadOptions.ignoreDefaults} to `true`
* to fully match the store with the on-disk state, set {@linkcode ReloadOptions | ignoreDefaults} to `true`
* - This method does not emit change events.
*
* @returns
+9 -2
View File
@@ -39,9 +39,14 @@ pub enum Error {
/// `reqwest` crate errors.
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
/// The platform was not found on the updater JSON response.
#[error("the platform `{0}` was not found on the response `platforms` object")]
/// The platform was not found in the updater JSON response.
#[error("the platform `{0}` was not found in the response `platforms` object")]
TargetNotFound(String),
/// Neither the platform nor the fallback platform was found in the updater JSON response.
#[error(
"None of the fallback platforms `{0:?}` were found in the response `platforms` object"
)]
TargetsNotFound(Vec<String>),
/// Download failed
#[error("`{0}`")]
Network(String),
@@ -69,6 +74,8 @@ pub enum Error {
AuthenticationFailed,
#[error("Failed to install .deb package")]
DebInstallFailed,
#[error("Failed to install package")]
PackageInstallFailed,
#[error("invalid updater binary format")]
InvalidUpdaterFormat,
#[error(transparent)]
+157 -81
View File
@@ -26,7 +26,13 @@ use reqwest::{
};
use semver::Version;
use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize};
use tauri::{utils::platform::current_exe, AppHandle, Resource, Runtime};
use tauri::{
utils::{
config::BundleType,
platform::{bundle_type, current_exe},
},
AppHandle, Resource, Runtime,
};
use time::OffsetDateTime;
use url::Url;
@@ -37,6 +43,31 @@ use crate::{
const UPDATER_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
#[derive(Copy, Clone)]
pub enum Installer {
AppImage,
Deb,
Rpm,
App,
Msi,
Nsis,
}
impl Installer {
fn name(self) -> &'static str {
match self {
Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::App => "app",
Self::Msi => "msi",
Self::Nsis => "nsis",
}
}
}
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct ReleaseManifestPlatform {
/// Download URL for the platform
@@ -265,13 +296,7 @@ impl UpdaterBuilder {
return Err(Error::EmptyEndpoints);
};
let arch = get_updater_arch().ok_or(Error::UnsupportedArch)?;
let (target, json_target) = if let Some(target) = self.target {
(target.clone(), target)
} else {
let target = get_updater_target().ok_or(Error::UnsupportedOs)?;
(target.to_string(), format!("{target}-{arch}"))
};
let arch = updater_arch().ok_or(Error::UnsupportedArch)?;
let executable_path = self.executable_path.clone().unwrap_or(current_exe()?);
@@ -294,8 +319,7 @@ impl UpdaterBuilder {
installer_args: self.installer_args,
current_exe_args: self.current_exe_args,
arch,
target,
json_target,
target: self.target,
headers: self.headers,
extract_path,
on_before_exit: self.on_before_exit,
@@ -327,10 +351,9 @@ pub struct Updater {
proxy: Option<Url>,
endpoints: Vec<Url>,
arch: &'static str,
// The `{{target}}` variable we replace in the endpoint
target: String,
// The value we search if the updater server returns a JSON with the `platforms` object
json_target: String,
// The `{{target}}` variable we replace in the endpoint and serach for in the JSON,
// this is either the user provided target or the current operating system by default
target: Option<String>,
headers: HeaderMap,
extract_path: PathBuf,
on_before_exit: Option<OnBeforeExit>,
@@ -359,12 +382,17 @@ impl Updater {
std::env::set_var("SSL_CERT_DIR", "/etc/ssl/certs");
}
}
let target = if let Some(target) = &self.target {
target
} else {
updater_os().ok_or(Error::UnsupportedOs)?
};
let mut remote_release: Option<RemoteRelease> = None;
let mut raw_json: Option<serde_json::Value> = None;
let mut last_error: Option<Error> = None;
for url in &self.endpoints {
// replace {{current_version}}, {{target}} and {{arch}} in the provided URL
// replace {{current_version}}, {{target}}, {{arch}} and {{bundle_type}} in the provided URL
// this is useful if we need to query example
// https://releases.myapp.com/update/{{target}}/{{arch}}/{{current_version}}
// will be translated into ->
@@ -376,17 +404,22 @@ impl Updater {
const CONTROLS_ADD: &AsciiSet = &CONTROLS.add(b'+');
let encoded_version = percent_encoding::percent_encode(version, CONTROLS_ADD);
let encoded_version = encoded_version.to_string();
let installer = installer_for_bundle_type(bundle_type())
.map(|i| i.name())
.unwrap_or("unknown");
let url: Url = url
.to_string()
// url::Url automatically url-encodes the path components
.replace("%7B%7Bcurrent_version%7D%7D", &encoded_version)
.replace("%7B%7Btarget%7D%7D", &self.target)
.replace("%7B%7Btarget%7D%7D", target)
.replace("%7B%7Barch%7D%7D", self.arch)
.replace("%7B%7Bbundle_type%7D%7D", installer)
// but not query parameters
.replace("{{current_version}}", &encoded_version)
.replace("{{target}}", &self.target)
.replace("{{target}}", target)
.replace("{{arch}}", self.arch)
.replace("{{bundle_type}}", installer)
.parse()?;
log::debug!("checking for updates {url}");
@@ -466,6 +499,9 @@ impl Updater {
None => release.version > self.current_version,
};
let installer = installer_for_bundle_type(bundle_type());
let (download_url, signature) = self.get_urls(&release, &installer)?;
let update = if should_update {
Some(Update {
run_on_main_thread: self.run_on_main_thread.clone(),
@@ -473,12 +509,12 @@ impl Updater {
on_before_exit: self.on_before_exit.clone(),
app_name: self.app_name.clone(),
current_version: self.current_version.to_string(),
target: self.target.clone(),
target: target.to_owned(),
extract_path: self.extract_path.clone(),
version: release.version.to_string(),
date: release.pub_date,
download_url: release.download_url(&self.json_target)?.to_owned(),
signature: release.signature(&self.json_target)?.to_owned(),
download_url: download_url.clone(),
signature: signature.to_owned(),
body: release.notes,
raw_json: raw_json.unwrap(),
timeout: None,
@@ -494,6 +530,38 @@ impl Updater {
Ok(update)
}
fn get_urls<'a>(
&self,
release: &'a RemoteRelease,
installer: &Option<Installer>,
) -> Result<(&'a Url, &'a String)> {
// Use the user provided target
if let Some(target) = &self.target {
return Ok((release.download_url(target)?, release.signature(target)?));
}
// Or else we search for [`{os}-{arch}-{installer}`, `{os}-{arch}`] in order
let os = updater_os().ok_or(Error::UnsupportedOs)?;
let arch = self.arch;
let mut targets = Vec::new();
if let Some(installer) = installer {
let installer = installer.name();
targets.push(format!("{os}-{arch}-{installer}"));
}
targets.push(format!("{os}-{arch}"));
for target in &targets {
log::debug!("Searching for updater target '{target}' in release data");
if let (Ok(download_url), Ok(signature)) =
(release.download_url(target), release.signature(target))
{
return Ok((download_url, signature));
};
}
Err(Error::TargetsNotFound(targets))
}
}
#[derive(Clone)]
@@ -511,7 +579,8 @@ pub struct Update {
pub version: String,
/// Update publish date
pub date: Option<OffsetDateTime>,
/// Target
/// The `{{target}}` variable we replace in the endpoint and search for in the JSON,
/// this is either the user provided target or the current operating system by default
pub target: String,
/// Download URL announced
pub download_url: Url,
@@ -852,11 +921,10 @@ impl Update {
/// └── ...
///
fn install_inner(&self, bytes: &[u8]) -> Result<()> {
if self.is_deb_package() {
self.install_deb(bytes)
} else {
// Handle AppImage or other formats
self.install_appimage(bytes)
match installer_for_bundle_type(bundle_type()) {
Some(Installer::Deb) => self.install_deb(bytes),
Some(Installer::Rpm) => self.install_rpm(bytes),
_ => self.install_appimage(bytes),
}
}
@@ -933,39 +1001,6 @@ impl Update {
Err(Error::TempDirNotOnSameMountPoint)
}
fn is_deb_package(&self) -> bool {
// First check if we're in a typical Debian installation path
let in_system_path = self
.extract_path
.to_str()
.map(|p| p.starts_with("/usr"))
.unwrap_or(false);
if !in_system_path {
return false;
}
// Then verify it's actually a Debian-based system by checking for dpkg
let dpkg_exists = std::path::Path::new("/var/lib/dpkg").exists();
let apt_exists = std::path::Path::new("/etc/apt").exists();
// Additional check for the package in dpkg database
let package_in_dpkg = if let Ok(output) = std::process::Command::new("dpkg")
.args(["-S", &self.extract_path.to_string_lossy()])
.output()
{
output.status.success()
} else {
false
};
// Consider it a deb package only if:
// 1. We're in a system path AND
// 2. We have Debian package management tools AND
// 3. The binary is tracked by dpkg
dpkg_exists && apt_exists && package_in_dpkg
}
fn install_deb(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .deb package
if !infer::archive::is_deb(bytes) {
@@ -973,6 +1008,18 @@ impl Update {
return Err(Error::InvalidUpdaterFormat);
}
self.try_tmp_locations(bytes, "dpkg", "-i")
}
fn install_rpm(&self, bytes: &[u8]) -> Result<()> {
// First verify the bytes are actually a .rpm package
if !infer::archive::is_rpm(bytes) {
return Err(Error::InvalidUpdaterFormat);
}
self.try_tmp_locations(bytes, "rpm", "-U")
}
fn try_tmp_locations(&self, bytes: &[u8], install_cmd: &str, install_arg: &str) -> Result<()> {
// Try different temp directories
let tmp_dir_locations = vec![
Box::new(|| Some(std::env::temp_dir())) as Box<dyn FnOnce() -> Option<PathBuf>>,
@@ -984,15 +1031,19 @@ impl Update {
for tmp_dir_location in tmp_dir_locations {
if let Some(path) = tmp_dir_location() {
if let Ok(tmp_dir) = tempfile::Builder::new()
.prefix("tauri_deb_update")
.prefix("tauri_rpm_update")
.tempdir_in(path)
{
let deb_path = tmp_dir.path().join("package.deb");
let pkg_path = tmp_dir.path().join("package.rpm");
// Try writing the .deb file
if std::fs::write(&deb_path, bytes).is_ok() {
if std::fs::write(&pkg_path, bytes).is_ok() {
// If write succeeds, proceed with installation
return self.try_install_with_privileges(&deb_path);
return self.try_install_with_privileges(
&pkg_path,
install_cmd,
install_arg,
);
}
// If write fails, continue to next temp location
}
@@ -1003,12 +1054,17 @@ impl Update {
Err(Error::TempDirNotFound)
}
fn try_install_with_privileges(&self, deb_path: &Path) -> Result<()> {
fn try_install_with_privileges(
&self,
pkg_path: &Path,
install_cmd: &str,
install_arg: &str,
) -> Result<()> {
// 1. First try using pkexec (graphical sudo prompt)
if let Ok(status) = std::process::Command::new("pkexec")
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.arg(install_cmd)
.arg(install_arg)
.arg(pkg_path)
.status()
{
if status.success() {
@@ -1019,7 +1075,7 @@ impl Update {
// 2. Try zenity or kdialog for a graphical sudo experience
if let Ok(password) = self.get_password_graphically() {
if self.install_with_sudo(deb_path, &password)? {
if self.install_with_sudo(pkg_path, &password, install_cmd, install_arg)? {
log::debug!("installed deb with GUI sudo");
return Ok(());
}
@@ -1027,16 +1083,16 @@ impl Update {
// 3. Final fallback: terminal sudo
let status = std::process::Command::new("sudo")
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.arg(install_cmd)
.arg(install_arg)
.arg(pkg_path)
.status()?;
if status.success() {
log::debug!("installed deb with sudo");
Ok(())
} else {
Err(Error::DebInstallFailed)
Err(Error::PackageInstallFailed)
}
}
@@ -1070,15 +1126,21 @@ impl Update {
Err(Error::AuthenticationFailed)
}
fn install_with_sudo(&self, deb_path: &Path, password: &str) -> Result<bool> {
fn install_with_sudo(
&self,
pkg_path: &Path,
password: &str,
install_cmd: &str,
install_arg: &str,
) -> Result<bool> {
use std::io::Write;
use std::process::{Command, Stdio};
let mut child = Command::new("sudo")
.arg("-S") // read password from stdin
.arg("dpkg")
.arg("-i")
.arg(deb_path)
.arg(install_cmd)
.arg(install_arg)
.arg(pkg_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
@@ -1086,7 +1148,7 @@ impl Update {
if let Some(mut stdin) = child.stdin.take() {
// Write password to stdin
writeln!(stdin, "{}", password)?;
writeln!(stdin, "{password}")?;
}
let status = child.wait()?;
@@ -1199,16 +1261,18 @@ impl Update {
}
}
/// Gets the target string used on the updater.
/// Gets the base target string used by the updater. If bundle type is available it
/// will be added to this string when selecting the download URL and signature.
/// `tauri::utils::platform::bundle_type` method is used to obtain current bundle type.
pub fn target() -> Option<String> {
if let (Some(target), Some(arch)) = (get_updater_target(), get_updater_arch()) {
if let (Some(target), Some(arch)) = (updater_os(), updater_arch()) {
Some(format!("{target}-{arch}"))
} else {
None
}
}
pub(crate) fn get_updater_target() -> Option<&'static str> {
fn updater_os() -> Option<&'static str> {
if cfg!(target_os = "linux") {
Some("linux")
} else if cfg!(target_os = "macos") {
@@ -1221,7 +1285,7 @@ pub(crate) fn get_updater_target() -> Option<&'static str> {
}
}
pub(crate) fn get_updater_arch() -> Option<&'static str> {
fn updater_arch() -> Option<&'static str> {
if cfg!(target_arch = "x86") {
Some("i686")
} else if cfg!(target_arch = "x86_64") {
@@ -1315,6 +1379,18 @@ impl<'de> Deserialize<'de> for RemoteRelease {
}
}
fn installer_for_bundle_type(bundle: Option<BundleType>) -> Option<Installer> {
match bundle? {
BundleType::Deb => Some(Installer::Deb),
BundleType::Rpm => Some(Installer::Rpm),
BundleType::AppImage => Some(Installer::AppImage),
BundleType::Msi => Some(Installer::Msi),
BundleType::Nsis => Some(Installer::Nsis),
BundleType::App => Some(Installer::App), // App is also returned for Dmg type
_ => None,
}
}
fn parse_version<'de, D>(deserializer: D) -> std::result::Result<Version, D::Error>
where
D: serde::Deserializer<'de>,
@@ -2,6 +2,7 @@
"identifier": "com.tauri.updater",
"plugins": {
"updater": {
"dangerousInsecureTransportProtocol": true,
"endpoints": ["http://localhost:3007"],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEUwNDRGMjkwRjg2MDhCRDAKUldUUWkyRDRrUEpFNEQ4SmdwcU5PaXl6R2ZRUUNvUnhIaVkwVUltV0NMaEx6VTkrWVhpT0ZqeEEK",
"windows": {
+162 -50
View File
@@ -17,6 +17,7 @@ use tauri::utils::config::{Updater, V1Compatible};
const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg==";
const UPDATED_EXIT_CODE: i32 = 0;
const ERROR_EXIT_CODE: i32 = 1;
const UP_TO_DATE_EXIT_CODE: i32 = 2;
#[derive(Serialize)]
@@ -48,7 +49,7 @@ struct Update {
fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTarget) {
let mut command = Command::new("cargo");
command
.args(["tauri", "build", "--debug", "--verbose"])
.args(["tauri", "build", "--verbose"])
.arg("--config")
.arg(serde_json::to_string(config).unwrap())
.env("TAURI_SIGNING_PRIVATE_KEY", UPDATER_PRIVATE_KEY)
@@ -80,6 +81,8 @@ fn build_app(cwd: &Path, config: &Config, bundle_updater: bool, target: BundleTa
#[derive(Copy, Clone)]
enum BundleTarget {
AppImage,
Deb,
Rpm,
App,
@@ -91,6 +94,8 @@ impl BundleTarget {
fn name(self) -> &'static str {
match self {
Self::AppImage => "appimage",
Self::Deb => "deb",
Self::Rpm => "rpm",
Self::App => "app",
Self::Msi => "msi",
Self::Nsis => "nsis",
@@ -109,57 +114,168 @@ impl Default for BundleTarget {
}
}
fn target_to_platforms(
update_platform: Option<String>,
signature: String,
) -> HashMap<String, PlatformUpdate> {
let mut platforms = HashMap::new();
if let Some(platform) = update_platform {
platforms.insert(
platform,
PlatformUpdate {
signature,
url: "http://localhost:3007/download",
with_elevated_task: false,
},
);
}
platforms
}
#[cfg(target_os = "linux")]
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
vec![(
BundleTarget::AppImage,
root_dir.join(format!(
"target/debug/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
)]
fn test_cases(
root_dir: &Path,
version: &str,
target: String,
) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
vec![
// update using fallback
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
Some(target.clone()),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// update using full name
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
Some(format!("{target}-{}", BundleTarget::AppImage.name())),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// no update
(
BundleTarget::AppImage,
root_dir.join(format!(
"target/release/bundle/appimage/app-updater_{version}_amd64.AppImage"
)),
None,
vec![ERROR_EXIT_CODE],
),
]
}
#[cfg(target_os = "macos")]
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
vec![(
BundleTarget::App,
root_dir.join("target/debug/bundle/macos/app-updater.app"),
)]
fn test_cases(
root_dir: &Path,
_version: &str,
target: String,
) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
vec![
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
Some(target.clone()),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// update with installer
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
Some(format!("{target}-{}", BundleTarget::App.name())),
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE],
),
// no update
(
BundleTarget::App,
root_dir.join("target/release/bundle/macos/app-updater.app"),
None,
vec![ERROR_EXIT_CODE],
),
]
}
#[cfg(target_os = "ios")]
fn bundle_paths(root_dir: &Path, _version: &str) -> Vec<(BundleTarget, PathBuf)> {
fn bundle_paths(
root_dir: &Path,
_version: &str,
v1compatible: bool,
) -> Vec<(BundleTarget, PathBuf)> {
vec![(
BundleTarget::App,
root_dir.join("target/debug/bundle/ios/app-updater.ipa"),
root_dir.join("target/release/bundle/ios/app-updater.ipa"),
)]
}
#[cfg(target_os = "android")]
fn bundle_path(root_dir: &Path, _version: &str) -> PathBuf {
root_dir.join("target/debug/bundle/android/app-updater.apk")
fn bundle_path(root_dir: &Path, _version: &str, v1compatible: bool) -> PathBuf {
root_dir.join("target/release/bundle/android/app-updater.apk")
}
#[cfg(windows)]
fn bundle_paths(root_dir: &Path, version: &str) -> Vec<(BundleTarget, PathBuf)> {
fn test_cases(
root_dir: &Path,
version: &str,
target: String,
) -> Vec<(BundleTarget, PathBuf, Option<String>, Vec<i32>)> {
vec![
(
BundleTarget::Nsis,
root_dir.join(format!(
"target/debug/bundle/nsis/app-updater_{version}_x64-setup.exe"
"target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)),
Some(target.clone()),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Nsis,
root_dir.join(format!(
"target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)),
Some(format!("{target}-{}", BundleTarget::Nsis.name())),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Nsis,
root_dir.join(format!(
"target/release/bundle/nsis/app-updater_{version}_x64-setup.exe"
)),
None,
vec![ERROR_EXIT_CODE],
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/debug/bundle/msi/app-updater_{version}_x64_en-US.msi"
"target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)),
Some(target.clone()),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)),
Some(format!("{target}-{}", BundleTarget::Msi.name())),
vec![UPDATED_EXIT_CODE],
),
(
BundleTarget::Msi,
root_dir.join(format!(
"target/release/bundle/msi/app-updater_{version}_x64_en-US.msi"
)),
None,
vec![ERROR_EXIT_CODE],
),
]
}
#[test]
#[ignore]
fn update_app() {
let target =
tauri_plugin_updater::target().expect("running updater test in an unsupported platform");
@@ -185,9 +301,6 @@ fn update_app() {
Updater::String(V1Compatible::V1Compatible)
);
// bundle app update
build_app(&manifest_dir, &config, true, Default::default());
let updater_zip_ext = if v1_compatible {
if cfg!(windows) {
Some("zip")
@@ -200,7 +313,13 @@ fn update_app() {
None
};
for (bundle_target, out_bundle_path) in bundle_paths(&root_dir, "1.0.0") {
for (bundle_target, out_bundle_path, update_platform, status_checks) in
test_cases(&root_dir, "1.0.0", target.clone())
{
// bundle app update
config.version = "1.0.0";
build_app(&manifest_dir, &config, true, BundleTarget::default());
let bundle_updater_ext = if v1_compatible {
out_bundle_path
.extension()
@@ -228,13 +347,11 @@ fn update_app() {
});
let out_updater_path = out_bundle_path.with_extension(updater_extension);
let updater_path = root_dir.join(format!(
"target/debug/{}",
"target/release/{}",
out_updater_path.file_name().unwrap().to_str().unwrap()
));
std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle");
let target = target.clone();
// start the updater server
let server = Arc::new(
tiny_http::Server::http("localhost:3007").expect("failed to start updater server"),
@@ -245,16 +362,9 @@ fn update_app() {
for request in server_.incoming_requests() {
match request.url() {
"/" => {
let mut platforms = HashMap::new();
let platforms =
target_to_platforms(update_platform.clone(), signature.clone());
platforms.insert(
target.clone(),
PlatformUpdate {
signature: signature.clone(),
url: "http://localhost:3007/download",
with_elevated_task: false,
},
);
let body = serde_json::to_vec(&Update {
version: "1.0.0",
date: time::OffsetDateTime::now_utc()
@@ -293,19 +403,12 @@ fn update_app() {
// bundle initial app version
build_app(&manifest_dir, &config, false, bundle_target);
let status_checks = if matches!(bundle_target, BundleTarget::Msi) {
// for msi we can't really check if the app was updated, because we can't change the install path
vec![UPDATED_EXIT_CODE]
} else {
vec![UPDATED_EXIT_CODE, UP_TO_DATE_EXIT_CODE]
};
for expected_exit_code in status_checks {
let mut binary_cmd = if cfg!(windows) {
Command::new(root_dir.join("target/debug/app-updater.exe"))
Command::new(root_dir.join("target/release/app-updater.exe"))
} else if cfg!(target_os = "macos") {
Command::new(
bundle_paths(&root_dir, "0.1.0")
test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1
@@ -313,11 +416,20 @@ fn update_app() {
)
} else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() {
let mut c = Command::new("xvfb-run");
c.arg("--auto-servernum")
.arg(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1);
c.arg("--auto-servernum").arg(
&test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1,
);
c
} else {
Command::new(&bundle_paths(&root_dir, "0.1.0").first().unwrap().1)
Command::new(
&test_cases(&root_dir, "0.1.0", target.clone())
.first()
.unwrap()
.1,
)
};
binary_cmd.env("TARGET", bundle_target.name());
@@ -327,7 +439,7 @@ fn update_app() {
if code != expected_exit_code {
panic!(
"failed to run app, expected exit code {expected_exit_code}, got {code}"
"failed to run app bundled as {}, expected exit code {expected_exit_code}, got {code}", bundle_target.name()
);
}
#[cfg(windows)]
@@ -9,7 +9,7 @@
"preview": "vite preview"
},
"devDependencies": {
"@tauri-apps/cli": "2.8.1",
"@tauri-apps/cli": "2.8.3",
"typescript": "^5.7.3",
"vite": "^7.0.4"
},
+270 -259
View File
File diff suppressed because it is too large Load Diff