snapshot of https://github.com/tauri-apps/tauri.git (dev, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
Some checks failed
Audit / audit-js (push) Failing after 0s
bench / bench (map[os:ubuntu-latest runner:xvfb-run --auto-servernum target:x86_64-unknown-linux-gnu], nightly) (push) Failing after 0s
covector version or publish / run-integration-tests (ubuntu-latest) (push) Failing after 0s
Audit / audit-rust (push) Failing after 0s
test `@tauri-apps/cli` / test (ubuntu-latest) (push) Failing after 0s
test `tauri-cli` / test (map[os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:ubuntu-latest target:x86_64-unknown-linux-gnu toolchain:1.90]) (push) Failing after 0s
test core / test (map[args:--no-default-features key:no-default], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
deploy schema worker / deploy (push) Failing after 0s
lint rust / clippy (map[cargo:cargo include: os:ubuntu-latest target:x86_64-unknown-linux-gnu]) (push) Failing after 0s
lint rust / clippy (map[cargo:cross include:--package api os:ubuntu-latest target:aarch64-linux-android]) (push) Failing after 0s
supply chain health status / check rust dependencies with cargo vet (push) Failing after 0s
test `@tauri-apps/api` e2e (mobile) / android (push) Failing after 0s
test `@tauri-apps/api` e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
test core / test (map[args:--all-features key:all], map[cargo:cross command:build os:ubuntu-latest target:aarch64-linux-android toolchain:1.90]) (push) Failing after 0s
Udeps / changes (push) Failing after 0s
Udeps / setup (push) Has been skipped
Udeps / udeps (push) Has been skipped
bench / bench (map[os:macos-latest runner: target:aarch64-apple-darwin], nightly) (push) Has been cancelled
bench / bench (map[os:windows-latest runner: target:x86_64-pc-windows-msvc], nightly) (push) Has been cancelled
covector version or publish / run-integration-tests (macos-latest) (push) Has been cancelled
covector version or publish / run-integration-tests (windows-latest) (push) Has been cancelled
covector version or publish / version-or-publish (push) Has been cancelled
test `@tauri-apps/cli` / test (macos-latest) (push) Has been cancelled
test `tauri-cli` / test (map[args:--no-default-features --features native-tls-vendored os:windows-latest restore-only:true target:aarch64-pc-windows-msvc]) (push) Has been cancelled
test `tauri-cli` / test (map[os:macos-latest target:x86_64-apple-darwin]) (push) Has been cancelled
test `tauri-cli` / test (map[os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:macos-latest target:aarch64-apple-darwin]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include: os:windows-latest target:x86_64-pc-windows-msvc]) (push) Has been cancelled
lint rust / clippy (map[cargo:cargo include:--package api os:macos-latest target:aarch64-apple-ios]) (push) Has been cancelled
test `@tauri-apps/api` e2e (mobile) / ios (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test `@tauri-apps/api` e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
test `@tauri-apps/cli` / test (windows-latest) (push) Has been cancelled
test core / test (map[args:--all-features key:all], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:build os:macos-latest target:aarch64-apple-ios toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:macos-14 target:aarch64-apple-darwin toolchain:1.90]) (push) Has been cancelled
test core / test (map[args:--no-default-features key:no-default], map[cargo:cargo command:test os:windows-latest target:x86_64-pc-windows-msvc toolchain:1.90]) (push) Has been cancelled
This commit is contained in:
1570
crates/tauri-bundler/CHANGELOG.md
Normal file
1570
crates/tauri-bundler/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
85
crates/tauri-bundler/Cargo.toml
Normal file
85
crates/tauri-bundler/Cargo.toml
Normal file
@@ -0,0 +1,85 @@
|
||||
[package]
|
||||
name = "tauri-bundler"
|
||||
version = "2.9.4"
|
||||
authors = [
|
||||
"George Burton <burtonageo@gmail.com>",
|
||||
"Tauri Programme within The Commons Conservancy",
|
||||
]
|
||||
categories = ["command-line-utilities", "development-tools::cargo-plugins"]
|
||||
license = "Apache-2.0 OR MIT"
|
||||
keywords = ["bundle", "cargo", "tauri"]
|
||||
repository = "https://github.com/tauri-apps/tauri"
|
||||
description = "Wrap rust executables in OS-specific app bundles for Tauri"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
exclude = ["CHANGELOG.md", "/target", "rustfmt.toml"]
|
||||
|
||||
[dependencies]
|
||||
tauri-utils = { workspace = true, features = ["resources"] }
|
||||
image = "0.25"
|
||||
flate2 = "1"
|
||||
thiserror = "2"
|
||||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
strsim = "0.11"
|
||||
tar = "0.4"
|
||||
walkdir = "2"
|
||||
bytesize = "2"
|
||||
handlebars = "6"
|
||||
tempfile = "3"
|
||||
log = { version = "0.4.21", features = ["kv"] }
|
||||
dirs = "7"
|
||||
os_pipe = "1"
|
||||
ureq = { version = "3", default-features = false, features = ["socks-proxy"] }
|
||||
native-tls = { version = "0.2", optional = true }
|
||||
hex = "0.4"
|
||||
semver = "1"
|
||||
sha1 = "0.11"
|
||||
sha2 = "0.11"
|
||||
zip = { version = "8", default-features = false, features = ["deflate"] }
|
||||
dunce = "1"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4", "v5"] }
|
||||
regex = "1"
|
||||
plist = "1"
|
||||
glob = "0.3"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies]
|
||||
bitness = "0.4"
|
||||
windows-registry = "0.6"
|
||||
|
||||
[target."cfg(target_os = \"windows\")".dependencies.windows-sys]
|
||||
version = "0.61"
|
||||
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"] }
|
||||
tauri-macos-sign = { workspace = true }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
heck = "0.5"
|
||||
ar = "0.9"
|
||||
md5 = "0.8"
|
||||
rpm = { version = "0.16", features = ["bzip2-compression"] }
|
||||
|
||||
[target."cfg(unix)".dependencies]
|
||||
which = "8"
|
||||
|
||||
[lib]
|
||||
name = "tauri_bundler"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
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"]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "3.5"
|
||||
192
crates/tauri-bundler/License-Apache.md
Normal file
192
crates/tauri-bundler/License-Apache.md
Normal file
@@ -0,0 +1,192 @@
|
||||
Copyright 2017 Cargo-Bundle developers
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
31
crates/tauri-bundler/License-MIT.md
Normal file
31
crates/tauri-bundler/License-MIT.md
Normal file
@@ -0,0 +1,31 @@
|
||||
Copyright 2017 Cargo-Bundle developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 - Present Tauri Apps Contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
127
crates/tauri-bundler/README.md
Normal file
127
crates/tauri-bundler/README.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Tauri Bundler
|
||||
|
||||
Wrap Rust executables in OS-specific app bundles.
|
||||
|
||||
## About
|
||||
|
||||
This is a fork of the awesome [cargo-bundle](https://github.com/burtonageo/cargo-bundle), turned into a library used by the [Tauri CLI](../tauri-cli).
|
||||
|
||||
### Stability
|
||||
|
||||
As it's intended to be used primarily in `tauri-cli`, the public API does not strictly adhere to SemVer. For example, minor releases may add new struct fields and change or remove Error enum variants.
|
||||
|
||||
## Configuration
|
||||
|
||||
Tauri automatically loads configurations from the `tauri.conf.json > bundle` object, but this library doesn't rely on it and can be used by non-Tauri apps.
|
||||
|
||||
### General settings
|
||||
|
||||
These settings apply to bundles for all (or most) OSes.
|
||||
|
||||
- `name`: The name of the built application. If this is not present, then it will use the `name` value from
|
||||
your `Cargo.toml` file.
|
||||
- `identifier`: [REQUIRED] A string that uniquely identifies your application,
|
||||
in reverse-DNS form (for example, `"com.example.appname"` or
|
||||
`"io.github.username.project"`). For OS X and iOS, this is used as the
|
||||
bundle's `CFBundleIdentifier` value; for Windows, this is hashed to create
|
||||
an application GUID.
|
||||
- `icon`: [OPTIONAL] The icons used for your application. This should be an array of file paths or globs (with images
|
||||
in various sizes/formats); `tauri-bundler` will automatically convert between image formats as necessary for
|
||||
different platforms. Supported formats include ICNS, ICO, PNG, and anything else that can be decoded by the
|
||||
[`image`](https://crates.io/crates/image) crate. Icons intended for high-resolution (e.g. Retina) displays
|
||||
should have a filename with `@2x` just before the extension (see example below).
|
||||
- `version`: [OPTIONAL] The version of the application. If this is not present, then it will use the `version`
|
||||
value from your `Cargo.toml` file.
|
||||
- `resources`: [OPTIONAL] List of files or directories which will be copied to the resources section of the
|
||||
bundle. Globs are supported.
|
||||
- `copyright`: [OPTIONAL] This contains a copyright string associated with your application.
|
||||
- `category`: [OPTIONAL] What kind of application this is. This can
|
||||
be a human-readable string (e.g. `"Puzzle game"`), or a Mac OS X
|
||||
LSApplicationCategoryType value
|
||||
(e.g. `"public.app-category.puzzle-games"`), or a GNOME desktop
|
||||
file category name (e.g. `"LogicGame"`), and `tauri-bundler` will
|
||||
automatically convert as needed for different platforms.
|
||||
- `short_description`: [OPTIONAL] A short, one-line description of the application. If this is not present, then it
|
||||
will use the `description` value from your `Cargo.toml` file.
|
||||
- `long_description`: [OPTIONAL] A longer, multi-line description of the application.
|
||||
|
||||
### Debian-specific settings
|
||||
|
||||
These settings are used only when bundling `deb` packages.
|
||||
|
||||
- `depends`: A list of strings indicating other packages (e.g. shared
|
||||
libraries) that this package depends on to be installed. If present, this
|
||||
forms the `Depends:` field of the `deb` package control file.
|
||||
|
||||
### Mac OS X-specific settings
|
||||
|
||||
These settings are used only when bundling `app` and `dmg` packages.
|
||||
|
||||
- `frameworks`: A list of strings indicating any Mac OS X frameworks that
|
||||
need to be bundled with the app. Each string can either be the name of a
|
||||
framework (without the `.framework` extension, e.g. `"SDL2"`), in which case
|
||||
`tauri-bundler` will search for that framework in the standard install
|
||||
locations (`~/Library/Frameworks/`, `/Library/Frameworks/`, and
|
||||
`/Network/Library/Frameworks/`), or a path to a specific framework bundle
|
||||
(e.g. `./data/frameworks/SDL2.framework`). Note that this setting just makes
|
||||
`tauri-bundler` copy the specified frameworks into the OS X app bundle (under
|
||||
`Foobar.app/Contents/Frameworks/`); you are still responsible for (1)
|
||||
arranging for the compiled binary to link against those frameworks (e.g. by
|
||||
emitting lines like `cargo:rustc-link-lib=framework=SDL2` from your
|
||||
`build.rs` script), and (2) embedding the correct rpath in your binary
|
||||
(e.g. by running `install_name_tool -add_rpath
|
||||
"@executable_path/../Frameworks" path/to/binary` after compiling).
|
||||
- `minimum_system_version`: A version string indicating the minimum Mac OS
|
||||
X version that the bundled app supports (e.g. `"10.11"`). If you are using
|
||||
this config field, you may also want have your `build.rs` script emit
|
||||
`cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.11` (or whatever version number
|
||||
you want) to ensure that the compiled binary has the same minimum version.
|
||||
- `license`: Path to the license file for the DMG bundle.
|
||||
- `exception_domain`: The exception domain to use on the macOS .app bundle. Allows communication to the outside world e.g. a web server you're shipping.
|
||||
- `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. See [Customizing the notarization workflow](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow) and search for `--list-providers` for more information how to obtain your provider short name.
|
||||
|
||||
### Example `tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"productName": "Your Awesome App",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.my.app",
|
||||
"app": {},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"shortDescription": "",
|
||||
"longDescription": "",
|
||||
"copyright": "Copyright (c) You 2021. All rights reserved.",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": ["./assets/**/*.png"],
|
||||
"deb": {
|
||||
"depends": ["debian-dependency1", "debian-dependency2"]
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "10.11",
|
||||
"license": "./LICENSE"
|
||||
},
|
||||
"externalBin": ["./sidecar-app"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
(c) 2017 - present, George Burton, Tauri-Apps Organization
|
||||
|
||||
This program is licensed either under the terms of the
|
||||
[Apache Software License](http://www.apache.org/licenses/LICENSE-2.0), or the
|
||||
[MIT License](https://opensource.org/licenses/MIT).
|
||||
|
||||
-> note, for bundle_dmg we have included a BSD 3 licensed binary `seticon`.
|
||||
https://github.com/sveinbjornt/osxiconutils/blob/master/seticon.m
|
||||
`tools/rust/cargo-tauri-bundle/src/bundle/templates/seticon`
|
||||
393
crates/tauri-bundler/src/bundle.rs
Normal file
393
crates/tauri-bundler/src/bundle.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
// 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
|
||||
|
||||
mod category;
|
||||
mod kmp;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
mod platform;
|
||||
mod settings;
|
||||
mod updater_bundle;
|
||||
mod windows;
|
||||
|
||||
use crate::error::ErrorExt;
|
||||
use anyhow::Context;
|
||||
use bytesize::ByteSize;
|
||||
use std::{
|
||||
fmt::Write,
|
||||
io::{Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
};
|
||||
use tauri_utils::{display_path, platform::Target as TargetPlatform};
|
||||
|
||||
pub use {
|
||||
category::AppCategory,
|
||||
settings::{
|
||||
AppImageSettings, BundleBinary, BundleSettings, CustomSignCommandSettings, DebianSettings,
|
||||
DmgSettings, Entitlements, IosSettings, MacOsSettings, NsisSettings, PackageSettings,
|
||||
PackageType, PlistKind, Position, RpmSettings, Settings, SettingsBuilder, Size,
|
||||
UpdaterSettings, WindowsSettings, WixLanguage, WixLanguageConfig, WixSettings,
|
||||
},
|
||||
windows::vswhere_path,
|
||||
};
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
/// Generated bundle metadata.
|
||||
#[derive(Debug)]
|
||||
pub struct Bundle {
|
||||
/// The package type.
|
||||
pub package_type: PackageType,
|
||||
/// All paths for this package.
|
||||
pub bundle_paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns the list of paths where the bundles can be found.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<Bundle>> {
|
||||
let mut package_types = settings.package_types()?;
|
||||
if package_types.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
package_types.sort_by_key(|a| a.priority());
|
||||
|
||||
let target_os = settings.target_platform();
|
||||
|
||||
if *target_os != TargetPlatform::current() {
|
||||
log::warn!(
|
||||
"Cross-platform compilation is experimental and does not support all features. Please use a matching host system for full compatibility."
|
||||
);
|
||||
}
|
||||
|
||||
// Sign windows binaries before the bundling step in case neither wix and nsis bundles are enabled
|
||||
sign_binaries_if_needed(settings, target_os)?;
|
||||
|
||||
let main_binary = settings.main_binary()?;
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
|
||||
// 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().context("failed to create temp file for main binary copy")?;
|
||||
let mut main_binary_original = std::fs::File::open(&main_binary_path)
|
||||
.fs_context("can't open main binary", &main_binary_path)?;
|
||||
std::io::copy(&mut main_binary_original, &mut main_binary_copy)?;
|
||||
|
||||
let mut bundles = Vec::<Bundle>::new();
|
||||
for package_type in &package_types {
|
||||
// bundle was already built! e.g. DMG already built .app
|
||||
if bundles.iter().any(|b| b.package_type == *package_type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if settings.binary_patching() {
|
||||
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"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
log::warn!(
|
||||
"Skipping binary patching for {} due to --no-binary-patching flag.",
|
||||
main_binary_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
// 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)?,
|
||||
#[cfg(target_os = "macos")]
|
||||
PackageType::IosBundle => macos::ios::bundle_project(settings)?,
|
||||
// dmg is dependent of MacOsBundle, we send our bundles to prevent rebuilding
|
||||
#[cfg(target_os = "macos")]
|
||||
PackageType::Dmg => {
|
||||
let bundled = macos::dmg::bundle_project(settings, &bundles)?;
|
||||
if !bundled.app.is_empty() {
|
||||
bundles.push(Bundle {
|
||||
package_type: PackageType::MacOsBundle,
|
||||
bundle_paths: bundled.app,
|
||||
});
|
||||
}
|
||||
bundled.dmg
|
||||
}
|
||||
|
||||
#[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")]
|
||||
PackageType::Deb => linux::debian::bundle_project(settings)?,
|
||||
#[cfg(target_os = "linux")]
|
||||
PackageType::Rpm => linux::rpm::bundle_project(settings)?,
|
||||
#[cfg(target_os = "linux")]
|
||||
PackageType::AppImage => linux::appimage::bundle_project(settings)?,
|
||||
_ => {
|
||||
log::warn!("ignoring {}", package_type.short_name());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
bundles.push(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() {
|
||||
if package_types.iter().any(|package_type| {
|
||||
if updater.v1_compatible {
|
||||
matches!(
|
||||
package_type,
|
||||
PackageType::AppImage
|
||||
| PackageType::MacOsBundle
|
||||
| PackageType::Nsis
|
||||
| PackageType::WindowsMsi
|
||||
| PackageType::Deb
|
||||
)
|
||||
} else {
|
||||
matches!(package_type, PackageType::MacOsBundle)
|
||||
}
|
||||
}) {
|
||||
let updater_paths = updater_bundle::bundle_project(settings, &bundles)?;
|
||||
bundles.push(Bundle {
|
||||
package_type: PackageType::Updater,
|
||||
bundle_paths: updater_paths,
|
||||
});
|
||||
} else if updater.v1_compatible
|
||||
|| !package_types.iter().any(|package_type| {
|
||||
// Self contained updater, no need to zip
|
||||
matches!(
|
||||
package_type,
|
||||
PackageType::AppImage | PackageType::Nsis | PackageType::WindowsMsi | PackageType::Deb
|
||||
)
|
||||
})
|
||||
{
|
||||
log::warn!(
|
||||
"The bundler was configured to create updater artifacts but no updater-enabled targets were built. Please enable one of these targets: app, appimage, msi, nsis"
|
||||
);
|
||||
}
|
||||
if updater.v1_compatible {
|
||||
log::warn!(
|
||||
"Legacy v1 compatible updater is deprecated and will be removed in v3, change bundle > createUpdaterArtifacts to true when your users are updated to the version with v2 updater plugin"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Clean up .app if only building dmg or updater
|
||||
if !package_types.contains(&PackageType::MacOsBundle) {
|
||||
if let Some(app_bundle_paths) = bundles
|
||||
.iter()
|
||||
.position(|b| b.package_type == PackageType::MacOsBundle)
|
||||
.map(|i| bundles.remove(i))
|
||||
.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),
|
||||
}
|
||||
.fs_context(
|
||||
"failed to clean the app bundle",
|
||||
app_bundle_path.to_path_buf(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if bundles.is_empty() {
|
||||
return Ok(bundles);
|
||||
}
|
||||
|
||||
let finished_bundles = bundles
|
||||
.iter()
|
||||
.filter(|b| b.package_type != PackageType::Updater)
|
||||
.count();
|
||||
let pluralised = if finished_bundles == 1 {
|
||||
"bundle"
|
||||
} else {
|
||||
"bundles"
|
||||
};
|
||||
|
||||
let mut printable_paths = String::new();
|
||||
for bundle in &bundles {
|
||||
for path in &bundle.bundle_paths {
|
||||
let note = if bundle.package_type == crate::PackageType::Updater {
|
||||
" (updater)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let path_display = display_path(path);
|
||||
let size = bundle_size(path)
|
||||
.map(|bytes| format!(" ({:.2})", ByteSize::b(bytes).display()))
|
||||
.unwrap_or_default();
|
||||
writeln!(printable_paths, " {path_display}{note}{size}").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(action = "Finished"; "{finished_bundles} {pluralised} at:\n{printable_paths}");
|
||||
|
||||
Ok(bundles)
|
||||
}
|
||||
|
||||
/// Total size in bytes of a bundle path, recursing into directories (e.g. macOS `.app`).
|
||||
fn bundle_size(path: &std::path::Path) -> crate::Result<u64> {
|
||||
let metadata = std::fs::symlink_metadata(path)?;
|
||||
if metadata.is_dir() {
|
||||
let mut total = 0;
|
||||
for entry in walkdir::WalkDir::new(path) {
|
||||
total += entry?.metadata()?.len();
|
||||
}
|
||||
Ok(total)
|
||||
} else {
|
||||
Ok(metadata.len())
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
let mut iter = settings.icon_files().peekable();
|
||||
|
||||
// if iter's first value is a None then there are no Icon files in the settings struct
|
||||
if iter.peek().is_none() {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
471
crates/tauri-bundler/src/bundle/category.rs
Normal file
471
crates/tauri-bundler/src/bundle/category.rs
Normal file
@@ -0,0 +1,471 @@
|
||||
// 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
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
const CONFIDENCE_THRESHOLD: f64 = 0.8;
|
||||
|
||||
const MACOS_APP_CATEGORY_PREFIX: &str = "public.app-category.";
|
||||
|
||||
// TODO: RIght now, these categories correspond to LSApplicationCategoryType
|
||||
// values for OS X. There are also some additional GNOME registered categories
|
||||
// that don't fit these; we should add those here too.
|
||||
/// The possible app categories.
|
||||
/// Corresponds to `LSApplicationCategoryType` on macOS and the GNOME desktop categories on Debian.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum AppCategory {
|
||||
Business,
|
||||
DeveloperTool,
|
||||
Education,
|
||||
Entertainment,
|
||||
Finance,
|
||||
Game,
|
||||
ActionGame,
|
||||
AdventureGame,
|
||||
ArcadeGame,
|
||||
BoardGame,
|
||||
CardGame,
|
||||
CasinoGame,
|
||||
DiceGame,
|
||||
EducationalGame,
|
||||
FamilyGame,
|
||||
KidsGame,
|
||||
MusicGame,
|
||||
PuzzleGame,
|
||||
RacingGame,
|
||||
RolePlayingGame,
|
||||
SimulationGame,
|
||||
SportsGame,
|
||||
StrategyGame,
|
||||
TriviaGame,
|
||||
WordGame,
|
||||
GraphicsAndDesign,
|
||||
HealthcareAndFitness,
|
||||
Lifestyle,
|
||||
Medical,
|
||||
Music,
|
||||
News,
|
||||
Photography,
|
||||
Productivity,
|
||||
Reference,
|
||||
SocialNetworking,
|
||||
Sports,
|
||||
Travel,
|
||||
Utility,
|
||||
Video,
|
||||
Weather,
|
||||
}
|
||||
|
||||
impl FromStr for AppCategory {
|
||||
type Err = Option<&'static str>;
|
||||
|
||||
/// Given a string, returns the `AppCategory` it refers to, or the closest
|
||||
/// string that the user might have intended (if any).
|
||||
fn from_str(input: &str) -> Result<AppCategory, Self::Err> {
|
||||
// Canonicalize input:
|
||||
let mut input = input.to_ascii_lowercase();
|
||||
if input.starts_with(MACOS_APP_CATEGORY_PREFIX) {
|
||||
input = input
|
||||
.split_at(MACOS_APP_CATEGORY_PREFIX.len())
|
||||
.1
|
||||
.to_string();
|
||||
}
|
||||
input = input.replace(' ', "");
|
||||
input = input.replace('-', "");
|
||||
|
||||
// Find best match:
|
||||
let mut best_confidence = 0.0;
|
||||
let mut best_category: Option<AppCategory> = None;
|
||||
for &(string, category) in CATEGORY_STRINGS.iter() {
|
||||
if input == string {
|
||||
return Ok(category);
|
||||
}
|
||||
let confidence = strsim::jaro_winkler(&input, string);
|
||||
if confidence >= CONFIDENCE_THRESHOLD && confidence > best_confidence {
|
||||
best_confidence = confidence;
|
||||
best_category = Some(category);
|
||||
}
|
||||
}
|
||||
Err(best_category.map(AppCategory::canonical))
|
||||
}
|
||||
}
|
||||
|
||||
impl AppCategory {
|
||||
/// Map an AppCategory to the string we recommend to use in Cargo.toml if
|
||||
/// the users misspells the category name.
|
||||
fn canonical(self) -> &'static str {
|
||||
match self {
|
||||
AppCategory::Business => "Business",
|
||||
AppCategory::DeveloperTool => "Developer Tool",
|
||||
AppCategory::Education => "Education",
|
||||
AppCategory::Entertainment => "Entertainment",
|
||||
AppCategory::Finance => "Finance",
|
||||
AppCategory::Game => "Game",
|
||||
AppCategory::ActionGame => "Action Game",
|
||||
AppCategory::AdventureGame => "Adventure Game",
|
||||
AppCategory::ArcadeGame => "Arcade Game",
|
||||
AppCategory::BoardGame => "Board Game",
|
||||
AppCategory::CardGame => "Card Game",
|
||||
AppCategory::CasinoGame => "Casino Game",
|
||||
AppCategory::DiceGame => "Dice Game",
|
||||
AppCategory::EducationalGame => "Educational Game",
|
||||
AppCategory::FamilyGame => "Family Game",
|
||||
AppCategory::KidsGame => "Kids Game",
|
||||
AppCategory::MusicGame => "Music Game",
|
||||
AppCategory::PuzzleGame => "Puzzle Game",
|
||||
AppCategory::RacingGame => "Racing Game",
|
||||
AppCategory::RolePlayingGame => "Role-Playing Game",
|
||||
AppCategory::SimulationGame => "Simulation Game",
|
||||
AppCategory::SportsGame => "Sports Game",
|
||||
AppCategory::StrategyGame => "Strategy Game",
|
||||
AppCategory::TriviaGame => "Trivia Game",
|
||||
AppCategory::WordGame => "Word Game",
|
||||
AppCategory::GraphicsAndDesign => "Graphics and Design",
|
||||
AppCategory::HealthcareAndFitness => "Healthcare and Fitness",
|
||||
AppCategory::Lifestyle => "Lifestyle",
|
||||
AppCategory::Medical => "Medical",
|
||||
AppCategory::Music => "Music",
|
||||
AppCategory::News => "News",
|
||||
AppCategory::Photography => "Photography",
|
||||
AppCategory::Productivity => "Productivity",
|
||||
AppCategory::Reference => "Reference",
|
||||
AppCategory::SocialNetworking => "Social Networking",
|
||||
AppCategory::Sports => "Sports",
|
||||
AppCategory::Travel => "Travel",
|
||||
AppCategory::Utility => "Utility",
|
||||
AppCategory::Video => "Video",
|
||||
AppCategory::Weather => "Weather",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an AppCategory to the closest set of Freedesktop registered
|
||||
/// categories that matches that category.
|
||||
///
|
||||
/// Cf <https://specifications.freedesktop.org/menu-spec/latest/>
|
||||
pub fn freedesktop_categories(self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "Office;",
|
||||
AppCategory::DeveloperTool => "Development;",
|
||||
AppCategory::Education => "Education;",
|
||||
AppCategory::Entertainment => "Network;",
|
||||
AppCategory::Finance => "Office;Finance;",
|
||||
AppCategory::Game => "Game;",
|
||||
AppCategory::ActionGame => "Game;ActionGame;",
|
||||
AppCategory::AdventureGame => "Game;AdventureGame;",
|
||||
AppCategory::ArcadeGame => "Game;ArcadeGame;",
|
||||
AppCategory::BoardGame => "Game;BoardGame;",
|
||||
AppCategory::CardGame => "Game;CardGame;",
|
||||
AppCategory::CasinoGame => "Game;",
|
||||
AppCategory::DiceGame => "Game;",
|
||||
AppCategory::EducationalGame => "Game;Education;",
|
||||
AppCategory::FamilyGame => "Game;",
|
||||
AppCategory::KidsGame => "Game;KidsGame;",
|
||||
AppCategory::MusicGame => "Game;",
|
||||
AppCategory::PuzzleGame => "Game;LogicGame;",
|
||||
AppCategory::RacingGame => "Game;",
|
||||
AppCategory::RolePlayingGame => "Game;RolePlaying;",
|
||||
AppCategory::SimulationGame => "Game;Simulation;",
|
||||
AppCategory::SportsGame => "Game;SportsGame;",
|
||||
AppCategory::StrategyGame => "Game;StrategyGame;",
|
||||
AppCategory::TriviaGame => "Game;",
|
||||
AppCategory::WordGame => "Game;",
|
||||
AppCategory::GraphicsAndDesign => "Graphics;",
|
||||
AppCategory::HealthcareAndFitness => "Science;",
|
||||
AppCategory::Lifestyle => "Education;",
|
||||
AppCategory::Medical => "Science;MedicalSoftware;",
|
||||
AppCategory::Music => "AudioVideo;Audio;Music;",
|
||||
AppCategory::News => "Network;News;",
|
||||
AppCategory::Photography => "Graphics;Photography;",
|
||||
AppCategory::Productivity => "Office;",
|
||||
AppCategory::Reference => "Education;",
|
||||
AppCategory::SocialNetworking => "Network;",
|
||||
AppCategory::Sports => "Education;Sports;",
|
||||
AppCategory::Travel => "Education;",
|
||||
AppCategory::Utility => "Utility;",
|
||||
AppCategory::Video => "AudioVideo;Video;",
|
||||
AppCategory::Weather => "Science;",
|
||||
}
|
||||
}
|
||||
|
||||
/// Map an AppCategory to the closest LSApplicationCategoryType value that
|
||||
/// matches that category.
|
||||
pub fn macos_application_category_type(self) -> &'static str {
|
||||
match &self {
|
||||
AppCategory::Business => "public.app-category.business",
|
||||
AppCategory::DeveloperTool => "public.app-category.developer-tools",
|
||||
AppCategory::Education => "public.app-category.education",
|
||||
AppCategory::Entertainment => "public.app-category.entertainment",
|
||||
AppCategory::Finance => "public.app-category.finance",
|
||||
AppCategory::Game => "public.app-category.games",
|
||||
AppCategory::ActionGame => "public.app-category.action-games",
|
||||
AppCategory::AdventureGame => "public.app-category.adventure-games",
|
||||
AppCategory::ArcadeGame => "public.app-category.arcade-games",
|
||||
AppCategory::BoardGame => "public.app-category.board-games",
|
||||
AppCategory::CardGame => "public.app-category.card-games",
|
||||
AppCategory::CasinoGame => "public.app-category.casino-games",
|
||||
AppCategory::DiceGame => "public.app-category.dice-games",
|
||||
AppCategory::EducationalGame => "public.app-category.educational-games",
|
||||
AppCategory::FamilyGame => "public.app-category.family-games",
|
||||
AppCategory::KidsGame => "public.app-category.kids-games",
|
||||
AppCategory::MusicGame => "public.app-category.music-games",
|
||||
AppCategory::PuzzleGame => "public.app-category.puzzle-games",
|
||||
AppCategory::RacingGame => "public.app-category.racing-games",
|
||||
AppCategory::RolePlayingGame => "public.app-category.role-playing-games",
|
||||
AppCategory::SimulationGame => "public.app-category.simulation-games",
|
||||
AppCategory::SportsGame => "public.app-category.sports-games",
|
||||
AppCategory::StrategyGame => "public.app-category.strategy-games",
|
||||
AppCategory::TriviaGame => "public.app-category.trivia-games",
|
||||
AppCategory::WordGame => "public.app-category.word-games",
|
||||
AppCategory::GraphicsAndDesign => "public.app-category.graphics-design",
|
||||
AppCategory::HealthcareAndFitness => "public.app-category.healthcare-fitness",
|
||||
AppCategory::Lifestyle => "public.app-category.lifestyle",
|
||||
AppCategory::Medical => "public.app-category.medical",
|
||||
AppCategory::Music => "public.app-category.music",
|
||||
AppCategory::News => "public.app-category.news",
|
||||
AppCategory::Photography => "public.app-category.photography",
|
||||
AppCategory::Productivity => "public.app-category.productivity",
|
||||
AppCategory::Reference => "public.app-category.reference",
|
||||
AppCategory::SocialNetworking => "public.app-category.social-networking",
|
||||
AppCategory::Sports => "public.app-category.sports",
|
||||
AppCategory::Travel => "public.app-category.travel",
|
||||
AppCategory::Utility => "public.app-category.utilities",
|
||||
AppCategory::Video => "public.app-category.video",
|
||||
AppCategory::Weather => "public.app-category.weather",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'d> serde::Deserialize<'d> for AppCategory {
|
||||
fn deserialize<D: serde::Deserializer<'d>>(deserializer: D) -> Result<AppCategory, D::Error> {
|
||||
deserializer.deserialize_str(AppCategoryVisitor { did_you_mean: None })
|
||||
}
|
||||
}
|
||||
|
||||
struct AppCategoryVisitor {
|
||||
did_you_mean: Option<&'static str>,
|
||||
}
|
||||
|
||||
impl serde::de::Visitor<'_> for AppCategoryVisitor {
|
||||
type Value = AppCategory;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.did_you_mean {
|
||||
Some(string) => write!(
|
||||
formatter,
|
||||
"a valid app category string (did you mean \"{string}\"?)"
|
||||
),
|
||||
None => write!(formatter, "a valid app category string"),
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_str<E: serde::de::Error>(mut self, value: &str) -> Result<AppCategory, E> {
|
||||
match AppCategory::from_str(value) {
|
||||
Ok(category) => Ok(category),
|
||||
Err(did_you_mean) => {
|
||||
self.did_you_mean = did_you_mean;
|
||||
let unexp = serde::de::Unexpected::Str(value);
|
||||
Err(serde::de::Error::invalid_value(unexp, &self))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const CATEGORY_STRINGS: &[(&str, AppCategory)] = &[
|
||||
("actiongame", AppCategory::ActionGame),
|
||||
("actiongames", AppCategory::ActionGame),
|
||||
("adventuregame", AppCategory::AdventureGame),
|
||||
("adventuregames", AppCategory::AdventureGame),
|
||||
("arcadegame", AppCategory::ArcadeGame),
|
||||
("arcadegames", AppCategory::ArcadeGame),
|
||||
("boardgame", AppCategory::BoardGame),
|
||||
("boardgames", AppCategory::BoardGame),
|
||||
("business", AppCategory::Business),
|
||||
("cardgame", AppCategory::CardGame),
|
||||
("cardgames", AppCategory::CardGame),
|
||||
("casinogame", AppCategory::CasinoGame),
|
||||
("casinogames", AppCategory::CasinoGame),
|
||||
("developer", AppCategory::DeveloperTool),
|
||||
("developertool", AppCategory::DeveloperTool),
|
||||
("developertools", AppCategory::DeveloperTool),
|
||||
("development", AppCategory::DeveloperTool),
|
||||
("dicegame", AppCategory::DiceGame),
|
||||
("dicegames", AppCategory::DiceGame),
|
||||
("education", AppCategory::Education),
|
||||
("educationalgame", AppCategory::EducationalGame),
|
||||
("educationalgames", AppCategory::EducationalGame),
|
||||
("entertainment", AppCategory::Entertainment),
|
||||
("familygame", AppCategory::FamilyGame),
|
||||
("familygames", AppCategory::FamilyGame),
|
||||
("finance", AppCategory::Finance),
|
||||
("fitness", AppCategory::HealthcareAndFitness),
|
||||
("game", AppCategory::Game),
|
||||
("games", AppCategory::Game),
|
||||
("graphicdesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsanddesign", AppCategory::GraphicsAndDesign),
|
||||
("graphicsdesign", AppCategory::GraphicsAndDesign),
|
||||
("healthcareandfitness", AppCategory::HealthcareAndFitness),
|
||||
("healthcarefitness", AppCategory::HealthcareAndFitness),
|
||||
("kidsgame", AppCategory::KidsGame),
|
||||
("kidsgames", AppCategory::KidsGame),
|
||||
("lifestyle", AppCategory::Lifestyle),
|
||||
("logicgame", AppCategory::PuzzleGame),
|
||||
("medical", AppCategory::Medical),
|
||||
("medicalsoftware", AppCategory::Medical),
|
||||
("music", AppCategory::Music),
|
||||
("musicgame", AppCategory::MusicGame),
|
||||
("musicgames", AppCategory::MusicGame),
|
||||
("news", AppCategory::News),
|
||||
("photography", AppCategory::Photography),
|
||||
("productivity", AppCategory::Productivity),
|
||||
("puzzlegame", AppCategory::PuzzleGame),
|
||||
("puzzlegames", AppCategory::PuzzleGame),
|
||||
("racinggame", AppCategory::RacingGame),
|
||||
("racinggames", AppCategory::RacingGame),
|
||||
("reference", AppCategory::Reference),
|
||||
("roleplaying", AppCategory::RolePlayingGame),
|
||||
("roleplayinggame", AppCategory::RolePlayingGame),
|
||||
("roleplayinggames", AppCategory::RolePlayingGame),
|
||||
("rpg", AppCategory::RolePlayingGame),
|
||||
("simulationgame", AppCategory::SimulationGame),
|
||||
("simulationgames", AppCategory::SimulationGame),
|
||||
("socialnetwork", AppCategory::SocialNetworking),
|
||||
("socialnetworking", AppCategory::SocialNetworking),
|
||||
("sports", AppCategory::Sports),
|
||||
("sportsgame", AppCategory::SportsGame),
|
||||
("sportsgames", AppCategory::SportsGame),
|
||||
("strategygame", AppCategory::StrategyGame),
|
||||
("strategygames", AppCategory::StrategyGame),
|
||||
("travel", AppCategory::Travel),
|
||||
("triviagame", AppCategory::TriviaGame),
|
||||
("triviagames", AppCategory::TriviaGame),
|
||||
("utilities", AppCategory::Utility),
|
||||
("utility", AppCategory::Utility),
|
||||
("video", AppCategory::Video),
|
||||
("weather", AppCategory::Weather),
|
||||
("wordgame", AppCategory::WordGame),
|
||||
("wordgames", AppCategory::WordGame),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::AppCategory;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn category_from_string_ok() {
|
||||
// Canonical name of category works:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Education"),
|
||||
Ok(AppCategory::Education)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Developer Tool"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
// Lowercase, spaces, and hyphens are fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str(" puzzle game "),
|
||||
Ok(AppCategory::PuzzleGame)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Role-playing game"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
// Using macOS LSApplicationCategoryType value is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("public.app-category.developer-tools"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("public.app-category.role-playing-games"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
// Using GNOME category name is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("Development"),
|
||||
Ok(AppCategory::DeveloperTool)
|
||||
);
|
||||
assert_eq!(
|
||||
AppCategory::from_str("LogicGame"),
|
||||
Ok(AppCategory::PuzzleGame)
|
||||
);
|
||||
// Using common abbreviations is fine:
|
||||
assert_eq!(
|
||||
AppCategory::from_str("RPG"),
|
||||
Ok(AppCategory::RolePlayingGame)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_from_string_did_you_mean() {
|
||||
assert_eq!(AppCategory::from_str("gaming"), Err(Some("Game")));
|
||||
assert_eq!(AppCategory::from_str("photos"), Err(Some("Photography")));
|
||||
assert_eq!(
|
||||
AppCategory::from_str("strategery"),
|
||||
Err(Some("Strategy Game"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_from_string_totally_wrong() {
|
||||
assert_eq!(AppCategory::from_str("fhqwhgads"), Err(None));
|
||||
assert_eq!(AppCategory::from_str("WHARRGARBL"), Err(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ls_application_category_type_round_trip() {
|
||||
let values = &[
|
||||
"public.app-category.business",
|
||||
"public.app-category.developer-tools",
|
||||
"public.app-category.education",
|
||||
"public.app-category.entertainment",
|
||||
"public.app-category.finance",
|
||||
"public.app-category.games",
|
||||
"public.app-category.action-games",
|
||||
"public.app-category.adventure-games",
|
||||
"public.app-category.arcade-games",
|
||||
"public.app-category.board-games",
|
||||
"public.app-category.card-games",
|
||||
"public.app-category.casino-games",
|
||||
"public.app-category.dice-games",
|
||||
"public.app-category.educational-games",
|
||||
"public.app-category.family-games",
|
||||
"public.app-category.kids-games",
|
||||
"public.app-category.music-games",
|
||||
"public.app-category.puzzle-games",
|
||||
"public.app-category.racing-games",
|
||||
"public.app-category.role-playing-games",
|
||||
"public.app-category.simulation-games",
|
||||
"public.app-category.sports-games",
|
||||
"public.app-category.strategy-games",
|
||||
"public.app-category.trivia-games",
|
||||
"public.app-category.word-games",
|
||||
"public.app-category.graphics-design",
|
||||
"public.app-category.healthcare-fitness",
|
||||
"public.app-category.lifestyle",
|
||||
"public.app-category.medical",
|
||||
"public.app-category.music",
|
||||
"public.app-category.news",
|
||||
"public.app-category.photography",
|
||||
"public.app-category.productivity",
|
||||
"public.app-category.reference",
|
||||
"public.app-category.social-networking",
|
||||
"public.app-category.sports",
|
||||
"public.app-category.travel",
|
||||
"public.app-category.utilities",
|
||||
"public.app-category.video",
|
||||
"public.app-category.weather",
|
||||
];
|
||||
// Test that if the user uses an LSApplicationCategoryType string as
|
||||
// the category string, they will get back that same string for the
|
||||
// macOS app bundle LSApplicationCategoryType.
|
||||
for &value in values.iter() {
|
||||
let category = AppCategory::from_str(value).expect(value);
|
||||
assert_eq!(category.macos_application_category_type(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
59
crates/tauri-bundler/src/bundle/kmp/mod.rs
Normal file
59
crates/tauri-bundler/src/bundle/kmp/mod.rs
Normal 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
|
||||
|
||||
// Knuth–Morris–Pratt 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
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
#! /bin/bash
|
||||
|
||||
# abort on all errors
|
||||
set -e
|
||||
|
||||
if [ "$DEBUG" != "" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
script=$(readlink -f "$0")
|
||||
|
||||
show_usage() {
|
||||
echo "Usage: $script --appdir <path to AppDir>"
|
||||
echo
|
||||
echo "Bundles GStreamer plugins into an AppDir"
|
||||
echo
|
||||
echo "Required variables:"
|
||||
echo " LINUXDEPLOY=\".../linuxdeploy\" path to linuxdeploy (e.g., AppImage); set automatically when plugin is run directly by linuxdeploy"
|
||||
echo
|
||||
echo "Optional variables:"
|
||||
echo " GSTREAMER_INCLUDE_BAD_PLUGINS=\"1\" (default: disabled; set to empty string or unset to disable)"
|
||||
echo " GSTREAMER_PLUGINS_DIR=\"...\" (directory containing GStreamer plugins; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_HELPERS_DIR=\"...\" (directory containing GStreamer helper tools like gst-plugin-scanner; default: guessed based on main distro architecture)"
|
||||
echo " GSTREAMER_VERSION=\"1.0\" (default: 1.0)"
|
||||
}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
case "$1" in
|
||||
--plugin-api-version)
|
||||
echo "0"
|
||||
exit 0
|
||||
;;
|
||||
--appdir)
|
||||
APPDIR="$2"
|
||||
shift
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Invalid argument: $1"
|
||||
echo
|
||||
show_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$APPDIR" == "" ]; then
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! which patchelf &>/dev/null && ! type patchelf &>/dev/null; then
|
||||
echo "Error: patchelf not found"
|
||||
echo
|
||||
show_usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ "$LINUXDEPLOY" == "" ]]; then
|
||||
echo "Error: \$LINUXDEPLOY not set"
|
||||
echo
|
||||
show_usage
|
||||
exit 3
|
||||
fi
|
||||
|
||||
mkdir -p "$APPDIR"
|
||||
|
||||
export GSTREAMER_VERSION="${GSTREAMER_VERSION:-1.0}"
|
||||
|
||||
plugins_target_dir="$APPDIR"/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
helpers_target_dir="$APPDIR"/usr/lib/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
|
||||
if [ "$GSTREAMER_PLUGINS_DIR" != "" ]; then
|
||||
plugins_dir="${GSTREAMER_PLUGINS_DIR}"
|
||||
elif [ -d /usr/lib/"$(uname -m)"-linux-gnu/gstreamer-"$GSTREAMER_VERSION" ]; then
|
||||
plugins_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer-"$GSTREAMER_VERSION"
|
||||
else
|
||||
plugins_dir=/usr/lib/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ "$GSTREAMER_HELPERS_DIR" != "" ]; then
|
||||
helpers_dir="${GSTREAMER_HELPERS_DIR}"
|
||||
else
|
||||
helpers_dir=/usr/lib/$(uname -m)-linux-gnu/gstreamer"$GSTREAMER_VERSION"/gstreamer-"$GSTREAMER_VERSION"
|
||||
fi
|
||||
|
||||
if [ ! -d "$plugins_dir" ]; then
|
||||
echo "Error: could not find plugins directory: $plugins_dir"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$plugins_target_dir"
|
||||
|
||||
echo "Copying plugins into $plugins_target_dir"
|
||||
for i in "$plugins_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying plugin: $i"
|
||||
cp "$i" "$plugins_target_dir"
|
||||
done
|
||||
|
||||
"$LINUXDEPLOY" --appdir "$APPDIR"
|
||||
|
||||
for i in "$plugins_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/..:$ORIGIN' "$i"
|
||||
done
|
||||
|
||||
mkdir -p "$helpers_target_dir"
|
||||
|
||||
echo "Copying helpers in $helpers_target_dir"
|
||||
for i in "$helpers_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
|
||||
echo "Copying helper: $i"
|
||||
cp "$i" "$helpers_target_dir"
|
||||
done
|
||||
|
||||
for i in "$helpers_target_dir"/*; do
|
||||
[ -d "$i" ] && continue
|
||||
[ ! -f "$i" ] && echo "File does not exist: $i" && continue
|
||||
(file "$i" | grep -v ELF --silent) && echo "Ignoring non ELF file: $i" && continue
|
||||
|
||||
echo "Manually setting rpath for $i"
|
||||
patchelf --set-rpath '$ORIGIN/../..' "$i"
|
||||
done
|
||||
|
||||
echo "Installing AppRun hook"
|
||||
mkdir -p "$APPDIR"/apprun-hooks
|
||||
|
||||
if [ "$GSTREAMER_VERSION" == "1.0" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
export GST_PLUGIN_PATH_1_0="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_1_0="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
elif [ "$GSTREAMER_VERSION" == "0.10" ]; then
|
||||
cat > "$APPDIR"/apprun-hooks/linuxdeploy-plugin-gstreamer.sh <<\EOF
|
||||
#! /bin/bash
|
||||
|
||||
export GST_REGISTRY_REUSE_PLUGIN_SCANNER="no"
|
||||
export GST_PLUGIN_SYSTEM_PATH_0_10="${APPDIR}/usr/lib/gstreamer-1.0"
|
||||
|
||||
export GST_PLUGIN_SCANNER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner"
|
||||
export GST_PTP_HELPER_0_10="${APPDIR}/usr/lib/gstreamer1.0/gstreamer-1.0/gst-ptp-helper"
|
||||
EOF
|
||||
else
|
||||
echo "Warning: unknown GStreamer version: $GSTREAMER_VERSION, cannot install AppRun hook"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,326 @@
|
||||
#! /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="${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" '{}' \;
|
||||
272
crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs
Normal file
272
crates/tauri-bundler/src/bundle/linux/appimage/linuxdeploy.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
// 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
|
||||
|
||||
use super::{super::debian, write_and_make_executable};
|
||||
use crate::{
|
||||
Settings,
|
||||
bundle::settings::Arch,
|
||||
error::Context,
|
||||
utils::{CommandExt, fs_utils, http_utils::download},
|
||||
};
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the AppImage was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// generate the deb binary name
|
||||
let appimage_arch: &str = match settings.binary_arch() {
|
||||
Arch::X86_64 => "amd64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhf",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let tools_arch = if settings.binary_arch() == Arch::Armhf {
|
||||
"armhf"
|
||||
} else {
|
||||
settings.target().split('-').next().unwrap()
|
||||
};
|
||||
|
||||
let output_path = settings.project_out_directory().join("bundle/appimage");
|
||||
if output_path.exists() {
|
||||
fs::remove_dir_all(&output_path)?;
|
||||
}
|
||||
|
||||
let tools_path = settings
|
||||
.local_tools_directory()
|
||||
.map(|d| d.join(".tauri"))
|
||||
.unwrap_or_else(|| {
|
||||
dirs::cache_dir().map_or_else(|| output_path.to_path_buf(), |p| p.join("tauri"))
|
||||
});
|
||||
|
||||
fs::create_dir_all(&tools_path)?;
|
||||
|
||||
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");
|
||||
|
||||
let main_binary = settings.main_binary()?;
|
||||
let product_name = settings.product_name();
|
||||
|
||||
let mut settings = settings.clone();
|
||||
if main_binary.name().contains(' ') {
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
let project_out_directory = settings.project_out_directory();
|
||||
|
||||
let main_binary_name_kebab = heck::AsKebabCase(main_binary.name()).to_string();
|
||||
let new_path = project_out_directory.join(&main_binary_name_kebab);
|
||||
fs::copy(main_binary_path, new_path)?;
|
||||
|
||||
let main_binary = settings.main_binary_mut()?;
|
||||
main_binary.set_name(main_binary_name_kebab);
|
||||
}
|
||||
|
||||
// generate deb_folder structure
|
||||
let (data_dir, icons) = debian::generate_data(&settings, &package_dir)
|
||||
.with_context(|| "Failed to build data folders and files")?;
|
||||
fs_utils::copy_custom_files(&settings.appimage().files, &data_dir)
|
||||
.with_context(|| "Failed to copy custom files")?;
|
||||
|
||||
fs::create_dir_all(&output_path)?;
|
||||
let app_dir_path = output_path.join(format!("{}.AppDir", settings.product_name()));
|
||||
let appimage_filename = format!(
|
||||
"{}_{}_{}.AppImage",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
appimage_arch
|
||||
);
|
||||
let appimage_path = output_path.join(&appimage_filename);
|
||||
fs_utils::create_dir(&app_dir_path, true)?;
|
||||
|
||||
fs::create_dir_all(&tools_path)?;
|
||||
let larger_icon = icons
|
||||
.iter()
|
||||
.filter(|i| i.width == i.height)
|
||||
.max_by_key(|i| i.width)
|
||||
.expect("couldn't find a square icon to use as AppImage icon");
|
||||
let larger_icon_path = larger_icon
|
||||
.path
|
||||
.strip_prefix(package_dir.join("data"))
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", appimage_filename, appimage_path.display());
|
||||
|
||||
let app_dir_usr = app_dir_path.join("usr/");
|
||||
let app_dir_usr_bin = app_dir_usr.join("bin/");
|
||||
let app_dir_usr_lib = app_dir_usr.join("lib/");
|
||||
|
||||
fs_utils::copy_dir(&data_dir.join("usr/"), &app_dir_usr)?;
|
||||
|
||||
// Using create_dir_all for a single dir so we don't get errors if the path already exists
|
||||
fs::create_dir_all(&app_dir_usr_bin)?;
|
||||
fs::create_dir_all(app_dir_usr_lib)?;
|
||||
|
||||
let search_dirs = [
|
||||
match settings.binary_arch() {
|
||||
Arch::X86_64 => "/usr/lib/x86_64-linux-gnu/",
|
||||
Arch::X86 => "/usr/lib/i386-linux-gnu/",
|
||||
Arch::AArch64 => "/usr/lib/aarch64-linux-gnu/",
|
||||
Arch::Armhf => "/usr/lib/arm-linux-gnueabihf/",
|
||||
_ => unreachable!(),
|
||||
},
|
||||
"/usr/lib64",
|
||||
"/usr/lib",
|
||||
"/usr/libexec",
|
||||
];
|
||||
|
||||
for file in [
|
||||
"WebKitNetworkProcess",
|
||||
"WebKitWebProcess",
|
||||
"injected-bundle/libwebkit2gtkinjectedbundle.so",
|
||||
] {
|
||||
for source in search_dirs.map(PathBuf::from) {
|
||||
// TODO: Check if it's the same dir name on all systems
|
||||
let source = source.join("webkit2gtk-4.1").join(file);
|
||||
if source.exists() {
|
||||
fs_utils::copy_file(
|
||||
&source,
|
||||
&app_dir_path.join(source.strip_prefix("/").unwrap()),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::copy(
|
||||
tools_path.join(format!("AppRun-{tools_arch}")),
|
||||
app_dir_path.join("AppRun"),
|
||||
)?;
|
||||
fs::copy(
|
||||
app_dir_path.join(larger_icon_path),
|
||||
app_dir_path.join(format!("{product_name}.png")),
|
||||
)?;
|
||||
std::os::unix::fs::symlink(format!("{product_name}.png"), app_dir_path.join(".DirIcon"))?;
|
||||
std::os::unix::fs::symlink(
|
||||
format!("usr/share/applications/{product_name}.desktop"),
|
||||
app_dir_path.join(format!("{product_name}.desktop")),
|
||||
)?;
|
||||
|
||||
let log_level = match settings.log_level() {
|
||||
log::Level::Error => "3",
|
||||
log::Level::Warn => "2",
|
||||
log::Level::Info => "1",
|
||||
_ => "0",
|
||||
};
|
||||
|
||||
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",
|
||||
log_level,
|
||||
"--appdir",
|
||||
&app_dir_path.display().to_string(),
|
||||
"--plugin",
|
||||
"gtk",
|
||||
]);
|
||||
if settings.appimage().bundle_media_framework {
|
||||
cmd.args(["--plugin", "gstreamer"]);
|
||||
}
|
||||
cmd.args(["--output", "appimage"]);
|
||||
|
||||
// Linuxdeploy logs everything into stderr so we have to ignore the output ourselves here
|
||||
if settings.log_level() == log::Level::Error {
|
||||
log::debug!(action = "Running"; "Command `linuxdeploy {}`", cmd.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
if !cmd.output()?.status.success() {
|
||||
return Err(crate::Error::GenericError(
|
||||
"failed to run linuxdeploy".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
cmd.output_ok()?;
|
||||
}
|
||||
|
||||
fs::remove_dir_all(&package_dir)?;
|
||||
Ok(vec![appimage_path])
|
||||
}
|
||||
|
||||
// returns the linuxdeploy path to keep linuxdeploy_arch contained
|
||||
fn prepare_tools(tools_path: &Path, arch: &str, verbose: bool) -> crate::Result<PathBuf> {
|
||||
let apprun = tools_path.join(format!("AppRun-{arch}"));
|
||||
if !apprun.exists() {
|
||||
let data = download(&format!(
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/apprun-old/AppRun-{arch}"
|
||||
))?;
|
||||
write_and_make_executable(&apprun, data)?;
|
||||
}
|
||||
|
||||
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"
|
||||
))?;
|
||||
write_and_make_executable(&linuxdeploy, data)?;
|
||||
}
|
||||
|
||||
let gtk = tools_path.join("linuxdeploy-plugin-gtk.sh");
|
||||
if !gtk.exists() {
|
||||
let data = download(
|
||||
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh",
|
||||
)?;
|
||||
write_and_make_executable(>k, data)?;
|
||||
}
|
||||
|
||||
let gstreamer = tools_path.join("linuxdeploy-plugin-gstreamer.sh");
|
||||
if !gstreamer.exists() {
|
||||
let data = download(
|
||||
"https://raw.githubusercontent.com/tauri-apps/linuxdeploy-plugin-gstreamer/master/linuxdeploy-plugin-gstreamer.sh",
|
||||
)?;
|
||||
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([
|
||||
"if=/dev/zero",
|
||||
"bs=1",
|
||||
"count=3",
|
||||
"seek=8",
|
||||
"conv=notrunc",
|
||||
&format!("of={}", linuxdeploy.display()),
|
||||
])
|
||||
.output();
|
||||
|
||||
Ok(linuxdeploy)
|
||||
}
|
||||
25
crates/tauri-bundler/src/bundle/linux/appimage/mod.rs
Normal file
25
crates/tauri-bundler/src/bundle/linux/appimage/mod.rs
Normal 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(())
|
||||
}
|
||||
403
crates/tauri-bundler/src/bundle/linux/debian.rs
Normal file
403
crates/tauri-bundler/src/bundle/linux/debian.rs
Normal file
@@ -0,0 +1,403 @@
|
||||
// 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
|
||||
|
||||
// The structure of a Debian package looks something like this:
|
||||
//
|
||||
// foobar_1.2.3_i386.deb # Actually an ar archive
|
||||
// debian-binary # Specifies deb format version (2.0 in our case)
|
||||
// control.tar.gz # Contains files controlling the installation:
|
||||
// control # Basic package metadata
|
||||
// md5sums # Checksums for files in data.tar.gz below
|
||||
// postinst # Post-installation script (optional)
|
||||
// prerm # Pre-uninstallation script (optional)
|
||||
// data.tar.gz # Contains files to be installed:
|
||||
// usr/bin/foobar # Binary executable file
|
||||
// usr/share/applications/foobar.desktop # Desktop file (for apps)
|
||||
// usr/share/icons/hicolor/... # Icon files (for apps)
|
||||
// usr/lib/foobar/... # Other resource files
|
||||
//
|
||||
// For cargo-bundle, we put bundle resource files under /usr/lib/package_name/,
|
||||
// and then generate the desktop file and control file from the bundle
|
||||
// metadata, as well as generating the md5sums file. Currently we do not
|
||||
// generate postinst or prerm files.
|
||||
|
||||
use super::freedesktop;
|
||||
use crate::{
|
||||
Settings,
|
||||
bundle::settings::Arch,
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
};
|
||||
use flate2::{Compression, write::GzEncoder};
|
||||
use tar::HeaderMode;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Write},
|
||||
os::unix::fs::{MetadataExt, OpenOptionsExt},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the DEB was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let arch = match settings.binary_arch() {
|
||||
Arch::X86_64 => "amd64",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "arm64",
|
||||
Arch::Armhf => "armhf",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let package_base_name = format!(
|
||||
"{}_{}_{}",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
arch
|
||||
);
|
||||
let package_name = format!("{package_base_name}.deb");
|
||||
|
||||
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).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).context("Failed to build data folders and files")?;
|
||||
fs_utils::copy_custom_files(&settings.deb().files, &data_dir)
|
||||
.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)
|
||||
.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")
|
||||
.context("Failed to create debian-binary file")?;
|
||||
|
||||
// Apply tar/gzip/ar to create the final package file.
|
||||
let control_tar_gz_path =
|
||||
tar_and_gzip_dir(control_dir).with_context(|| "Failed to tar/gzip control directory")?;
|
||||
let data_tar_gz_path =
|
||||
tar_and_gzip_dir(data_dir).with_context(|| "Failed to tar/gzip data directory")?;
|
||||
create_archive(
|
||||
vec![debian_binary_path, control_tar_gz_path, data_tar_gz_path],
|
||||
&package_path,
|
||||
)
|
||||
.with_context(|| "Failed to create package archive")?;
|
||||
Ok(vec![package_path])
|
||||
}
|
||||
|
||||
/// Generate the debian data folders and files.
|
||||
pub fn generate_data(
|
||||
settings: &Settings,
|
||||
package_dir: &Path,
|
||||
) -> crate::Result<(PathBuf, Vec<freedesktop::Icon>)> {
|
||||
// Generate data files.
|
||||
let data_dir = package_dir.join("data");
|
||||
let bin_dir = data_dir.join("usr/bin");
|
||||
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
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")?;
|
||||
|
||||
settings
|
||||
.copy_binaries(&bin_dir)
|
||||
.with_context(|| "Failed to copy external binaries")?;
|
||||
|
||||
let icons = freedesktop::copy_icon_files(settings, &data_dir)
|
||||
.with_context(|| "Failed to create icon files")?;
|
||||
freedesktop::generate_desktop_file(settings, &settings.deb().desktop_template, &data_dir)
|
||||
.with_context(|| "Failed to create desktop file")?;
|
||||
generate_changelog_file(settings, &data_dir)
|
||||
.with_context(|| "Failed to create changelog.gz file")?;
|
||||
|
||||
Ok((data_dir, icons))
|
||||
}
|
||||
|
||||
/// Generate the Changelog file by compressing, to be stored at /usr/share/doc/package-name/changelog.gz. See
|
||||
/// <https://www.debian.org/doc/debian-policy/ch-docs.html#changelog-files-and-release-notes>
|
||||
fn generate_changelog_file(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
|
||||
if let Some(changelog_src_path) = &settings.deb().changelog {
|
||||
let mut src_file = File::open(changelog_src_path)?;
|
||||
let product_name = settings.product_name();
|
||||
let dest_path = data_dir.join(format!("usr/share/doc/{product_name}/changelog.gz"));
|
||||
|
||||
let changelog_file = fs_utils::create_file(&dest_path)?;
|
||||
let mut gzip_encoder = GzEncoder::new(changelog_file, Compression::new(9));
|
||||
io::copy(&mut src_file, &mut gzip_encoder)?;
|
||||
|
||||
let mut changelog_file = gzip_encoder.finish()?;
|
||||
changelog_file.flush()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Generates the debian control file and stores it under the `control_dir`.
|
||||
fn generate_control_file(
|
||||
settings: &Settings,
|
||||
arch: &str,
|
||||
control_dir: &Path,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<()> {
|
||||
// For more information about the format of this file, see
|
||||
// https://www.debian.org/doc/debian-policy/ch-controlfields.html
|
||||
let dest_path = control_dir.join("control");
|
||||
let mut file = fs_utils::create_file(&dest_path)?;
|
||||
let package = heck::AsKebabCase(settings.product_name());
|
||||
writeln!(file, "Package: {package}")?;
|
||||
writeln!(file, "Version: {}", settings.version_string())?;
|
||||
writeln!(file, "Architecture: {arch}")?;
|
||||
// Installed-Size must be divided by 1024, see https://www.debian.org/doc/debian-policy/ch-controlfields.html#installed-size
|
||||
writeln!(file, "Installed-Size: {}", total_dir_size(data_dir)? / 1024)?;
|
||||
let authors = settings
|
||||
.authors_comma_separated()
|
||||
.or_else(|| settings.publisher().map(ToString::to_string))
|
||||
.unwrap_or_else(|| {
|
||||
settings
|
||||
.bundle_identifier()
|
||||
.split('.')
|
||||
.nth(1)
|
||||
.unwrap_or(settings.bundle_identifier())
|
||||
.to_string()
|
||||
});
|
||||
|
||||
writeln!(file, "Maintainer: {authors}")?;
|
||||
if let Some(section) = &settings.deb().section {
|
||||
writeln!(file, "Section: {section}")?;
|
||||
}
|
||||
if let Some(priority) = &settings.deb().priority {
|
||||
writeln!(file, "Priority: {priority}")?;
|
||||
} else {
|
||||
writeln!(file, "Priority: optional")?;
|
||||
}
|
||||
|
||||
if let Some(homepage) = settings.homepage_url() {
|
||||
writeln!(file, "Homepage: {homepage}")?;
|
||||
}
|
||||
|
||||
let dependencies = settings.deb().depends.as_ref().cloned().unwrap_or_default();
|
||||
if !dependencies.is_empty() {
|
||||
writeln!(file, "Depends: {}", dependencies.join(", "))?;
|
||||
}
|
||||
let dependencies = settings
|
||||
.deb()
|
||||
.recommends
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !dependencies.is_empty() {
|
||||
writeln!(file, "Recommends: {}", dependencies.join(", "))?;
|
||||
}
|
||||
let provides = settings
|
||||
.deb()
|
||||
.provides
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !provides.is_empty() {
|
||||
writeln!(file, "Provides: {}", provides.join(", "))?;
|
||||
}
|
||||
let conflicts = settings
|
||||
.deb()
|
||||
.conflicts
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !conflicts.is_empty() {
|
||||
writeln!(file, "Conflicts: {}", conflicts.join(", "))?;
|
||||
}
|
||||
let replaces = settings
|
||||
.deb()
|
||||
.replaces
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
if !replaces.is_empty() {
|
||||
writeln!(file, "Replaces: {}", replaces.join(", "))?;
|
||||
}
|
||||
let mut short_description = settings.short_description().trim();
|
||||
if short_description.is_empty() {
|
||||
short_description = "(none)";
|
||||
}
|
||||
let mut long_description = settings.long_description().unwrap_or("").trim();
|
||||
if long_description.is_empty() {
|
||||
long_description = "(none)";
|
||||
}
|
||||
writeln!(file, "Description: {short_description}")?;
|
||||
for line in long_description.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
writeln!(file, " .")?;
|
||||
} else {
|
||||
writeln!(file, " {line}")?;
|
||||
}
|
||||
}
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_scripts(settings: &Settings, control_dir: &Path) -> crate::Result<()> {
|
||||
if let Some(script_path) = &settings.deb().pre_install_script {
|
||||
let dest_path = control_dir.join("preinst");
|
||||
create_script_file_from_path(script_path, &dest_path)?
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.deb().post_install_script {
|
||||
let dest_path = control_dir.join("postinst");
|
||||
create_script_file_from_path(script_path, &dest_path)?
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.deb().pre_remove_script {
|
||||
let dest_path = control_dir.join("prerm");
|
||||
create_script_file_from_path(script_path, &dest_path)?
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.deb().post_remove_script {
|
||||
let dest_path = control_dir.join("postrm");
|
||||
create_script_file_from_path(script_path, &dest_path)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_script_file_from_path(from: &PathBuf, to: &PathBuf) -> crate::Result<()> {
|
||||
let mut from = File::open(from)?;
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.mode(0o755)
|
||||
.open(to)?;
|
||||
std::io::copy(&mut from, &mut file)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an `md5sums` file in the `control_dir` containing the MD5 checksums
|
||||
/// for each file within the `data_dir`.
|
||||
fn generate_md5sums(control_dir: &Path, data_dir: &Path) -> crate::Result<()> {
|
||||
let md5sums_path = control_dir.join("md5sums");
|
||||
let mut md5sums_file = fs_utils::create_file(&md5sums_path)?;
|
||||
for entry in WalkDir::new(data_dir) {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let mut file = File::open(path)?;
|
||||
let mut hash = md5::Context::new();
|
||||
io::copy(&mut file, &mut hash)?;
|
||||
for byte in hash.finalize().iter() {
|
||||
write!(md5sums_file, "{byte:02x}")?;
|
||||
}
|
||||
let rel_path = path.strip_prefix(data_dir)?;
|
||||
let path_str = rel_path.to_str().ok_or_else(|| {
|
||||
let msg = format!("Non-UTF-8 path: {rel_path:?}");
|
||||
io::Error::new(io::ErrorKind::InvalidData, msg)
|
||||
})?;
|
||||
writeln!(md5sums_file, " {path_str}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copy the bundle's resource files into an appropriate directory under the
|
||||
/// `data_dir`.
|
||||
fn copy_resource_files(settings: &Settings, data_dir: &Path) -> crate::Result<()> {
|
||||
let resource_dir = data_dir.join("usr/lib").join(settings.product_name());
|
||||
settings.copy_resources(&resource_dir)
|
||||
}
|
||||
|
||||
/// Create an empty file at the given path, creating any parent directories as
|
||||
/// needed, then write `data` into the file.
|
||||
fn create_file_with_data<P: AsRef<Path>>(path: P, data: &str) -> crate::Result<()> {
|
||||
let mut file = fs_utils::create_file(path.as_ref())?;
|
||||
file.write_all(data.as_bytes())?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Computes the total size, in bytes, of the given directory and all of its
|
||||
/// contents.
|
||||
fn total_dir_size(dir: &Path) -> crate::Result<u64> {
|
||||
let mut total: u64 = 0;
|
||||
for entry in WalkDir::new(dir) {
|
||||
total += entry?.metadata()?.len();
|
||||
}
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
/// Writes a tar file to the given writer containing the given directory.
|
||||
fn create_tar_from_dir<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let mut tar_builder = tar::Builder::new(dest_file);
|
||||
for entry in WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
if src_path == src_dir {
|
||||
continue;
|
||||
}
|
||||
let dest_path = src_path.strip_prefix(src_dir)?;
|
||||
let stat = fs::metadata(src_path)?;
|
||||
let mut header = tar::Header::new_gnu();
|
||||
header.set_metadata_in_mode(&stat, HeaderMode::Deterministic);
|
||||
header.set_mtime(stat.mtime() as u64);
|
||||
|
||||
if entry.file_type().is_dir() {
|
||||
tar_builder.append_data(&mut header, dest_path, &mut io::empty())?;
|
||||
} else {
|
||||
let mut src_file = fs::File::open(src_path)?;
|
||||
tar_builder.append_data(&mut header, dest_path, &mut src_file)?;
|
||||
}
|
||||
}
|
||||
let dest_file = tar_builder.into_inner()?;
|
||||
Ok(dest_file)
|
||||
}
|
||||
|
||||
/// Creates a `.tar.gz` file from the given directory (placing the new file
|
||||
/// within the given directory's parent directory), then deletes the original
|
||||
/// directory and returns the path to the new file.
|
||||
fn tar_and_gzip_dir<P: AsRef<Path>>(src_dir: P) -> crate::Result<PathBuf> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let dest_path = src_dir.with_extension("tar.gz");
|
||||
let dest_file = fs_utils::create_file(&dest_path)?;
|
||||
let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
|
||||
let gzip_encoder = create_tar_from_dir(src_dir, gzip_encoder)?;
|
||||
let mut dest_file = gzip_encoder.finish()?;
|
||||
dest_file.flush()?;
|
||||
Ok(dest_path)
|
||||
}
|
||||
|
||||
/// Creates an `ar` archive from the given source files and writes it to the
|
||||
/// given destination path.
|
||||
fn create_archive(srcs: Vec<PathBuf>, dest: &Path) -> crate::Result<()> {
|
||||
let mut builder = ar::Builder::new(fs_utils::create_file(dest)?);
|
||||
for path in &srcs {
|
||||
builder.append_path(path)?;
|
||||
}
|
||||
builder.into_inner()?.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
Categories={{categories}}
|
||||
{{#if comment}}
|
||||
Comment={{comment}}
|
||||
{{/if}}
|
||||
Exec={{exec}}
|
||||
StartupWMClass={{exec}}
|
||||
Icon={{icon}}
|
||||
Name={{name}}
|
||||
Terminal=false
|
||||
Type=Application
|
||||
{{#if mime_type}}
|
||||
MimeType={{mime_type}}
|
||||
{{/if}}
|
||||
186
crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs
Normal file
186
crates/tauri-bundler/src/bundle/linux/freedesktop/mod.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
// 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
|
||||
|
||||
//! This module provides utilities helping the packaging of desktop
|
||||
//! applications for Linux:
|
||||
//!
|
||||
//! - Generation of [desktop entries] (`.desktop` files)
|
||||
//! - Copy of icons in the [icons file hierarchy]
|
||||
//!
|
||||
//! The specifications are developed and hosted at [freedesktop.org].
|
||||
//!
|
||||
//! [freedesktop.org]: https://www.freedesktop.org
|
||||
//! [desktop entries]: https://www.freedesktop.org/wiki/Specifications/desktop-entry-spec/
|
||||
//! [icons file hierarchy]: https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#icon_lookup
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{File, read_to_string};
|
||||
use std::io::BufReader;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use handlebars::Handlebars;
|
||||
use image::{self, ImageDecoder, codecs::png::PngDecoder};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
Settings,
|
||||
error::Context,
|
||||
utils::{self, fs_utils},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Icon {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub is_high_density: bool,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
/// Generate the icon files, and returns a map where keys are the icons and
|
||||
/// values are their current (source) path.
|
||||
pub fn list_icon_files(
|
||||
settings: &Settings,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<BTreeMap<Icon, PathBuf>> {
|
||||
let base_dir = data_dir.join("usr/share/icons/hicolor");
|
||||
let main_binary_name = settings.main_binary_name()?;
|
||||
let get_dest_path = |width: u32, height: u32, is_high_density: bool| {
|
||||
base_dir.join(format!(
|
||||
"{}x{}{}/apps/{}.png",
|
||||
width,
|
||||
height,
|
||||
if is_high_density { "@2" } else { "" },
|
||||
main_binary_name
|
||||
))
|
||||
};
|
||||
let mut icons = BTreeMap::new();
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() != Some(OsStr::new("png")) {
|
||||
continue;
|
||||
}
|
||||
// Put file in scope so that it's closed when copying it
|
||||
let icon = {
|
||||
let decoder = PngDecoder::new(BufReader::new(File::open(&icon_path)?))?;
|
||||
let width = decoder.dimensions().0;
|
||||
let height = decoder.dimensions().1;
|
||||
let is_high_density = utils::is_retina(&icon_path);
|
||||
let dest_path = get_dest_path(width, height, is_high_density);
|
||||
Icon {
|
||||
width,
|
||||
height,
|
||||
is_high_density,
|
||||
path: dest_path,
|
||||
}
|
||||
};
|
||||
icons.entry(icon).or_insert(icon_path);
|
||||
}
|
||||
|
||||
Ok(icons)
|
||||
}
|
||||
|
||||
/// Generate the icon files and store them under the `data_dir`.
|
||||
pub fn copy_icon_files(settings: &Settings, data_dir: &Path) -> crate::Result<Vec<Icon>> {
|
||||
let icons = list_icon_files(settings, data_dir)?;
|
||||
for (icon, src) in &icons {
|
||||
fs_utils::copy_file(src, &icon.path)?;
|
||||
}
|
||||
|
||||
Ok(icons.into_keys().collect())
|
||||
}
|
||||
|
||||
/// Generate the application desktop file and store it under the `data_dir`.
|
||||
/// Returns the path of the resulting file (source path) and the destination
|
||||
/// path in the package.
|
||||
pub fn generate_desktop_file(
|
||||
settings: &Settings,
|
||||
custom_template_path: &Option<PathBuf>,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<(PathBuf, PathBuf)> {
|
||||
let bin_name = settings.main_binary_name()?;
|
||||
|
||||
let product_name = settings.product_name();
|
||||
let desktop_file_name = format!("{product_name}.desktop");
|
||||
let path = PathBuf::from("usr/share/applications").join(desktop_file_name);
|
||||
let dest_path = PathBuf::from("/").join(&path);
|
||||
let file_path = data_dir.join(&path);
|
||||
let file = &mut fs_utils::create_file(&file_path)?;
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_escape_fn(handlebars::no_escape);
|
||||
if let Some(template) = custom_template_path {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", read_to_string(template)?)
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup custom handlebar template")?;
|
||||
} else {
|
||||
handlebars
|
||||
.register_template_string("main.desktop", include_str!("./main.desktop"))
|
||||
.map_err(Into::into)
|
||||
.context("Failed to setup default handlebar template")?;
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct DesktopTemplateParams<'a> {
|
||||
categories: &'a str,
|
||||
comment: Option<&'a str>,
|
||||
exec: &'a str,
|
||||
icon: &'a str,
|
||||
name: &'a str,
|
||||
mime_type: Option<String>,
|
||||
long_description: String,
|
||||
}
|
||||
|
||||
let mut mime_type: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(associations) = settings.file_associations() {
|
||||
mime_type.extend(
|
||||
associations
|
||||
.iter()
|
||||
.filter_map(|association| association.mime_type.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(protocols) = settings.deep_link_protocols() {
|
||||
mime_type.extend(
|
||||
protocols
|
||||
.iter()
|
||||
.flat_map(|protocol| &protocol.schemes)
|
||||
.map(|s| format!("x-scheme-handler/{s}")),
|
||||
);
|
||||
}
|
||||
|
||||
let mime_type = (!mime_type.is_empty()).then_some(mime_type.join(";"));
|
||||
|
||||
let bin_name_exec = if bin_name.contains(' ') {
|
||||
format!("\"{bin_name}\"")
|
||||
} else {
|
||||
bin_name.to_string()
|
||||
};
|
||||
|
||||
handlebars.render_to_write(
|
||||
"main.desktop",
|
||||
&DesktopTemplateParams {
|
||||
categories: settings
|
||||
.app_category()
|
||||
.map(|app_category| app_category.freedesktop_categories())
|
||||
.unwrap_or(""),
|
||||
comment: if !settings.short_description().is_empty() {
|
||||
Some(settings.short_description())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
exec: &bin_name_exec,
|
||||
icon: bin_name,
|
||||
name: settings.product_name(),
|
||||
mime_type,
|
||||
long_description: settings.long_description().unwrap_or_default().to_string(),
|
||||
},
|
||||
file,
|
||||
)?;
|
||||
|
||||
Ok((file_path, dest_path))
|
||||
}
|
||||
9
crates/tauri-bundler/src/bundle/linux/mod.rs
Normal file
9
crates/tauri-bundler/src/bundle/linux/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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
|
||||
|
||||
pub mod appimage;
|
||||
pub mod debian;
|
||||
pub mod freedesktop;
|
||||
pub mod rpm;
|
||||
243
crates/tauri-bundler/src/bundle/linux/rpm.rs
Normal file
243
crates/tauri-bundler/src/bundle/linux/rpm.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
// 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
|
||||
|
||||
use crate::{Settings, bundle::settings::Arch, error::ErrorExt};
|
||||
|
||||
use rpm::{self, Dependency, FileMode, FileOptions, signature::pgp};
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use tauri_utils::config::RpmCompression;
|
||||
|
||||
use super::freedesktop;
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the RPM was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
let product_name = settings.product_name();
|
||||
let version = settings.version_string();
|
||||
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",
|
||||
Arch::X86 => "i386",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Armhf => "armhfp",
|
||||
Arch::Armel => "armel",
|
||||
Arch::Riscv64 => "riscv64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let summary = settings.short_description().trim();
|
||||
|
||||
let package_base_name = format!("{product_name}-{version}-{release}.{arch}");
|
||||
let package_name = format!("{package_base_name}.rpm");
|
||||
|
||||
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).fs_context(
|
||||
"Failed to remove old package directory",
|
||||
package_dir.clone(),
|
||||
)?;
|
||||
}
|
||||
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());
|
||||
|
||||
let license = settings.license().unwrap_or_default();
|
||||
let name = heck::AsKebabCase(settings.product_name()).to_string();
|
||||
|
||||
let compression = settings
|
||||
.rpm()
|
||||
.compression
|
||||
.map(|c| match c {
|
||||
RpmCompression::Gzip { level } => rpm::CompressionWithLevel::Gzip(level),
|
||||
RpmCompression::Zstd { level } => rpm::CompressionWithLevel::Zstd(level),
|
||||
RpmCompression::Xz { level } => rpm::CompressionWithLevel::Xz(level),
|
||||
RpmCompression::Bzip2 { level } => rpm::CompressionWithLevel::Bzip2(level),
|
||||
_ => rpm::CompressionWithLevel::None,
|
||||
})
|
||||
// This matches .deb compression. On a 240MB source binary the bundle will be 100KB larger than rpm's default while reducing build times by ~25%.
|
||||
// TODO: Default to Zstd in v3 to match rpm-rs new default in 0.16
|
||||
.unwrap_or(rpm::CompressionWithLevel::Gzip(6));
|
||||
|
||||
let mut builder = rpm::PackageBuilder::new(&name, version, &license, arch, summary)
|
||||
.epoch(epoch)
|
||||
.release(release)
|
||||
.compression(compression);
|
||||
|
||||
if let Some(description) = settings.long_description() {
|
||||
builder = builder.description(description);
|
||||
}
|
||||
|
||||
if let Some(homepage) = settings.homepage_url() {
|
||||
builder = builder.url(homepage);
|
||||
}
|
||||
|
||||
// Add requirements
|
||||
for dep in settings.rpm().depends.as_ref().cloned().unwrap_or_default() {
|
||||
builder = builder.requires(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add provides
|
||||
for dep in settings
|
||||
.rpm()
|
||||
.provides
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.provides(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add recommends
|
||||
for dep in settings
|
||||
.rpm()
|
||||
.recommends
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.recommends(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add conflicts
|
||||
for dep in settings
|
||||
.rpm()
|
||||
.conflicts
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.conflicts(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add obsoletes
|
||||
for dep in settings
|
||||
.rpm()
|
||||
.obsoletes
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
builder = builder.obsoletes(Dependency::any(dep));
|
||||
}
|
||||
|
||||
// Add binaries
|
||||
for bin in settings.binaries() {
|
||||
let src = settings.binary_path(bin);
|
||||
let dest = Path::new("/usr/bin").join(bin.name());
|
||||
builder = builder.with_file(src, FileOptions::new(dest.to_string_lossy()))?;
|
||||
}
|
||||
|
||||
// Add external binaries
|
||||
for src in settings.external_binaries() {
|
||||
let src = src?;
|
||||
let dest = Path::new("/usr/bin").join(
|
||||
src
|
||||
.file_name()
|
||||
.expect("failed to extract external binary filename")
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{}", settings.target()), ""),
|
||||
);
|
||||
builder = builder.with_file(&src, FileOptions::new(dest.to_string_lossy()))?;
|
||||
}
|
||||
|
||||
// Add scripts
|
||||
if let Some(script_path) = &settings.rpm().pre_install_script {
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
builder = builder.pre_install_script(script);
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.rpm().post_install_script {
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
builder = builder.post_install_script(script);
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.rpm().pre_remove_script {
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
builder = builder.pre_uninstall_script(script);
|
||||
}
|
||||
|
||||
if let Some(script_path) = &settings.rpm().post_remove_script {
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
builder = builder.post_uninstall_script(script);
|
||||
}
|
||||
|
||||
// Add resources
|
||||
if settings.resource_files().count() > 0 {
|
||||
let resource_dir = Path::new("/usr/lib").join(settings.product_name());
|
||||
// Create an empty file, needed to add a directory to the RPM package
|
||||
// (cf https://github.com/rpm-rs/rpm/issues/177)
|
||||
let empty_file_path = &package_dir.join("empty");
|
||||
File::create(empty_file_path)?;
|
||||
// Then add the resource directory `/usr/lib/<product_name>` to the package.
|
||||
builder = builder.with_file(
|
||||
empty_file_path,
|
||||
FileOptions::new(resource_dir.to_string_lossy()).mode(FileMode::Dir { permissions: 0o755 }),
|
||||
)?;
|
||||
// Then add the resources files in that directory
|
||||
for resource in settings.resource_files().iter() {
|
||||
let resource = resource?;
|
||||
let dest = resource_dir.join(resource.target());
|
||||
builder = builder.with_file(resource.path(), FileOptions::new(dest.to_string_lossy()))?;
|
||||
}
|
||||
}
|
||||
|
||||
// Add Desktop entry file
|
||||
let (desktop_src_path, desktop_dest_path) =
|
||||
freedesktop::generate_desktop_file(settings, &settings.rpm().desktop_template, &package_dir)?;
|
||||
builder = builder.with_file(
|
||||
desktop_src_path,
|
||||
FileOptions::new(desktop_dest_path.to_string_lossy()),
|
||||
)?;
|
||||
|
||||
// Add icons
|
||||
for (icon, src) in &freedesktop::list_icon_files(settings, &PathBuf::from("/"))? {
|
||||
builder = builder.with_file(src, FileOptions::new(icon.path.to_string_lossy()))?;
|
||||
}
|
||||
|
||||
// Add custom files
|
||||
for (rpm_path, src_path) in settings.rpm().files.iter() {
|
||||
if src_path.is_file() {
|
||||
builder = builder.with_file(src_path, FileOptions::new(rpm_path.to_string_lossy()))?;
|
||||
} else {
|
||||
for entry in walkdir::WalkDir::new(src_path) {
|
||||
let entry_path = entry?.into_path();
|
||||
if entry_path.is_file() {
|
||||
let dest_path = rpm_path.join(entry_path.strip_prefix(src_path).unwrap());
|
||||
builder =
|
||||
builder.with_file(&entry_path, FileOptions::new(dest_path.to_string_lossy()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let pkg = if let Ok(raw_secret_key) = env::var("TAURI_SIGNING_RPM_KEY") {
|
||||
let mut signer = pgp::Signer::load_from_asc(&raw_secret_key)?;
|
||||
if let Ok(passphrase) = env::var("TAURI_SIGNING_RPM_KEY_PASSPHRASE") {
|
||||
signer = signer.with_key_passphrase(passphrase);
|
||||
}
|
||||
builder.build_and_sign(signer)?
|
||||
} else {
|
||||
builder.build()?
|
||||
};
|
||||
|
||||
let mut f = fs::File::create(&package_path)?;
|
||||
pkg.write(&mut f)?;
|
||||
Ok(vec![package_path])
|
||||
}
|
||||
684
crates/tauri-bundler/src/bundle/macos/app.rs
Normal file
684
crates/tauri-bundler/src/bundle/macos/app.rs
Normal file
@@ -0,0 +1,684 @@
|
||||
// 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
|
||||
|
||||
// A macOS application bundle package is laid out like:
|
||||
//
|
||||
// foobar.app # Actually a directory
|
||||
// Contents # A further subdirectory
|
||||
// Info.plist # An xml file containing the app's metadata
|
||||
// MacOS # A directory to hold executable binary files
|
||||
// foobar # The main binary executable of the app
|
||||
// foobar_helper # A helper application, possibly providing a CLI
|
||||
// Resources # Data files such as images, sounds, translations and nib files
|
||||
// en.lproj # Folder containing english translation strings/data
|
||||
// Frameworks # A directory containing private frameworks (shared libraries)
|
||||
// ... # Any other optional files the developer wants to place here
|
||||
//
|
||||
// See https://developer.apple.com/go/?id=bundle-structure for a full
|
||||
// explanation.
|
||||
//
|
||||
// Currently, cargo-bundle does not support Frameworks, nor does it support placing arbitrary
|
||||
// files into the `Contents` directory of the bundle.
|
||||
|
||||
use super::{
|
||||
icon::{app_icon_name_from_assets_car, create_assets_car_file, create_icns_file},
|
||||
sign::{SignTarget, notarize, notarize_auth, notarize_without_stapling, sign},
|
||||
};
|
||||
use crate::{
|
||||
Error::GenericError,
|
||||
Settings,
|
||||
bundle::settings::PlistKind,
|
||||
error::{Context, ErrorExt, NotarizeAuthError},
|
||||
utils::{CommandExt, fs_utils},
|
||||
};
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
const NESTED_CODE_FOLDER: [&str; 6] = [
|
||||
"MacOS",
|
||||
"Frameworks",
|
||||
"Plugins",
|
||||
"Helpers",
|
||||
"XPCServices",
|
||||
"Libraries",
|
||||
];
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the .app was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
// we should use the bundle name (App name) as a MacOS standard.
|
||||
// version or platform shouldn't be included in the App name.
|
||||
let app_product_name = format!("{}.app", settings.product_name());
|
||||
|
||||
let app_bundle_path = settings
|
||||
.project_out_directory()
|
||||
.join("bundle/macos")
|
||||
.join(&app_product_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", app_product_name, app_bundle_path.display());
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.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)
|
||||
.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 =
|
||||
create_icns_file(&resources_dir, settings).with_context(|| "Failed to create app icon")?;
|
||||
|
||||
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")?;
|
||||
sign_paths.extend(framework_paths);
|
||||
|
||||
settings.copy_resources(&resources_dir)?;
|
||||
|
||||
let bin_paths = settings
|
||||
.copy_binaries(&bin_dir)
|
||||
.with_context(|| "Failed to copy external binaries")?;
|
||||
sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget {
|
||||
path,
|
||||
is_an_executable: true,
|
||||
}));
|
||||
|
||||
let bin_paths = copy_binaries_to_bundle(&bundle_directory, settings)?;
|
||||
sign_paths.extend(bin_paths.into_iter().map(|path| SignTarget {
|
||||
path,
|
||||
is_an_executable: true,
|
||||
}));
|
||||
|
||||
copy_custom_files_to_bundle(&bundle_directory, settings)?;
|
||||
|
||||
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 {
|
||||
path: app_bundle_path.clone(),
|
||||
is_an_executable: true,
|
||||
});
|
||||
|
||||
// Remove extra attributes, which could cause codesign to fail
|
||||
// https://developer.apple.com/library/archive/qa/qa1940/_index.html
|
||||
remove_extra_attr(&app_bundle_path)?;
|
||||
|
||||
// sign application
|
||||
sign(&keychain, sign_paths, settings)?;
|
||||
|
||||
// notarization is required for distribution
|
||||
match notarize_auth() {
|
||||
Ok(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(e.into());
|
||||
} else {
|
||||
log::warn!("skipping app notarization, {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(vec![app_bundle_path])
|
||||
}
|
||||
|
||||
fn remove_extra_attr(app_bundle_path: &Path) -> crate::Result<()> {
|
||||
Command::new("xattr")
|
||||
.arg("-crs")
|
||||
.arg(app_bundle_path)
|
||||
.output_ok()
|
||||
.context("failed to remove extra attributes from app bundle")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Copies the app's binaries to the bundle.
|
||||
fn copy_binaries_to_bundle(
|
||||
bundle_directory: &Path,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
let mut paths = Vec::new();
|
||||
let dest_dir = bundle_directory.join("MacOS");
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
let dest_path = dest_dir.join(bin.name());
|
||||
fs_utils::copy_file(&bin_path, &dest_path)
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
|
||||
paths.push(dest_path);
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
contents_path
|
||||
};
|
||||
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 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(())
|
||||
}
|
||||
|
||||
// Creates the Info.plist file.
|
||||
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();
|
||||
plist.insert("CFBundleDevelopmentRegion".into(), "English".into());
|
||||
plist.insert("CFBundleDisplayName".into(), settings.product_name().into());
|
||||
plist.insert(
|
||||
"CFBundleExecutable".into(),
|
||||
settings.main_binary_name()?.into(),
|
||||
);
|
||||
plist.insert(
|
||||
"CFBundleIdentifier".into(),
|
||||
settings.bundle_identifier().into(),
|
||||
);
|
||||
plist.insert("CFBundleInfoDictionaryVersion".into(), "6.0".into());
|
||||
if let Some(bundle_name) = settings
|
||||
.macos()
|
||||
.bundle_name
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.product_name())
|
||||
.into()
|
||||
{
|
||||
plist.insert("CFBundleName".into(), bundle_name.into());
|
||||
}
|
||||
plist.insert("CFBundlePackageType".into(), "APPL".into());
|
||||
plist.insert(
|
||||
"CFBundleShortVersionString".into(),
|
||||
settings.version_string().into(),
|
||||
);
|
||||
plist.insert(
|
||||
"CFBundleVersion".into(),
|
||||
settings
|
||||
.macos()
|
||||
.bundle_version
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.version_string())
|
||||
.into(),
|
||||
);
|
||||
plist.insert("CSResourcesFileMapped".into(), true.into());
|
||||
if let Some(category) = settings.app_category() {
|
||||
plist.insert(
|
||||
"LSApplicationCategoryType".into(),
|
||||
category.macos_application_category_type().into(),
|
||||
);
|
||||
}
|
||||
if let Some(version) = settings.macos().minimum_system_version.clone() {
|
||||
plist.insert("LSMinimumSystemVersion".into(), version.into());
|
||||
}
|
||||
|
||||
if let Some(associations) = settings.file_associations() {
|
||||
if let Some(file_associations_plist) =
|
||||
tauri_utils::config::file_associations_plist(associations)
|
||||
{
|
||||
if let Some(plist_dict) = file_associations_plist.as_dictionary() {
|
||||
for (key, value) in plist_dict {
|
||||
plist.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.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(
|
||||
"CFBundleURLSchemes".into(),
|
||||
plist::Value::Array(
|
||||
protocol
|
||||
.schemes
|
||||
.iter()
|
||||
.map(|s| s.to_string().into())
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
dict.insert(
|
||||
"CFBundleURLName".into(),
|
||||
protocol
|
||||
.name
|
||||
.clone()
|
||||
.unwrap_or(format!(
|
||||
"{} {}",
|
||||
settings.bundle_identifier(),
|
||||
protocol.schemes[0]
|
||||
))
|
||||
.into(),
|
||||
);
|
||||
dict.insert("CFBundleTypeRole".into(), protocol.role.to_string().into());
|
||||
plist::Value::Dictionary(dict)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
plist.insert("LSRequiresCarbon".into(), true.into());
|
||||
plist.insert("NSHighResolutionCapable".into(), true.into());
|
||||
if let Some(copyright) = settings.copyright_string() {
|
||||
plist.insert("NSHumanReadableCopyright".into(), copyright.into());
|
||||
}
|
||||
|
||||
if let Some(exception_domain) = settings.macos().exception_domain.clone() {
|
||||
let mut security = plist::Dictionary::new();
|
||||
let mut domain = plist::Dictionary::new();
|
||||
domain.insert("NSExceptionAllowsInsecureHTTPLoads".into(), true.into());
|
||||
domain.insert("NSIncludesSubdomains".into(), true.into());
|
||||
|
||||
let mut exception_domains = plist::Dictionary::new();
|
||||
exception_domains.insert(exception_domain, domain.into());
|
||||
security.insert("NSExceptionDomains".into(), exception_domains.into());
|
||||
plist.insert("NSAppTransportSecurity".into(), security.into());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plist::Value::Dictionary(plist).to_file_xml(bundle_dir.join("Info.plist"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Copies the framework under `{src_dir}/{framework}.framework` to `{dest_dir}/{framework}.framework`.
|
||||
fn copy_framework_from(dest_dir: &Path, framework: &str, src_dir: &Path) -> crate::Result<bool> {
|
||||
let src_name = format!("{framework}.framework");
|
||||
let src_path = src_dir.join(&src_name);
|
||||
if src_path.exists() {
|
||||
fs_utils::copy_dir(&src_path, &dest_dir.join(&src_name))?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Copies the macOS application bundle frameworks to the .app
|
||||
fn copy_frameworks_to_bundle(
|
||||
bundle_directory: &Path,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<Vec<SignTarget>> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
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(&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);
|
||||
let src_name = src_path
|
||||
.file_name()
|
||||
.expect("Couldn't get framework filename");
|
||||
let dest_path = dest_dir.join(src_name);
|
||||
fs_utils::copy_dir(&src_path, &dest_path)?;
|
||||
add_framework_sign_path(&src_path, &dest_path, &mut paths);
|
||||
continue;
|
||||
} else if framework.ends_with(".dylib") {
|
||||
let src_path = PathBuf::from(framework);
|
||||
if !src_path.exists() {
|
||||
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);
|
||||
fs_utils::copy_file(&src_path, &dest_path)?;
|
||||
paths.push(SignTarget {
|
||||
path: dest_path,
|
||||
is_an_executable: false,
|
||||
});
|
||||
continue;
|
||||
} else if framework.contains('/') {
|
||||
return Err(GenericError(format!(
|
||||
"Framework path should have .framework extension: {framework}"
|
||||
)));
|
||||
}
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
if copy_framework_from(&dest_dir, framework, &home_dir.join("Library/Frameworks/"))? {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if copy_framework_from(&dest_dir, framework, &PathBuf::from("/Library/Frameworks/"))?
|
||||
|| copy_framework_from(
|
||||
&dest_dir,
|
||||
framework,
|
||||
&PathBuf::from("/Network/Library/Frameworks/"),
|
||||
)?
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Err(GenericError(format!(
|
||||
"Could not locate framework: {framework}"
|
||||
)));
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// Recursively add framework's sign paths.
|
||||
/// If the framework has multiple versions, it will sign "Current" version by default.
|
||||
fn add_framework_sign_path(
|
||||
framework_root: &Path,
|
||||
dest_path: &Path,
|
||||
sign_paths: &mut Vec<SignTarget>,
|
||||
) {
|
||||
if framework_root.join("Versions/Current").exists() {
|
||||
add_nested_code_sign_path(
|
||||
&framework_root.join("Versions/Current"),
|
||||
&dest_path.join("Versions/Current"),
|
||||
sign_paths,
|
||||
);
|
||||
} else {
|
||||
add_nested_code_sign_path(framework_root, dest_path, sign_paths);
|
||||
}
|
||||
sign_paths.push(SignTarget {
|
||||
path: dest_path.into(),
|
||||
is_an_executable: false,
|
||||
});
|
||||
}
|
||||
|
||||
/// Recursively add executable bundle's sign path (.xpc, .app).
|
||||
fn add_executable_bundle_sign_path(
|
||||
bundle_root: &Path,
|
||||
dest_path: &Path,
|
||||
sign_paths: &mut Vec<SignTarget>,
|
||||
) {
|
||||
if bundle_root.join("Contents").exists() {
|
||||
add_nested_code_sign_path(
|
||||
&bundle_root.join("Contents"),
|
||||
&dest_path.join("Contents"),
|
||||
sign_paths,
|
||||
);
|
||||
} else {
|
||||
add_nested_code_sign_path(bundle_root, dest_path, sign_paths);
|
||||
}
|
||||
sign_paths.push(SignTarget {
|
||||
path: dest_path.into(),
|
||||
is_an_executable: true,
|
||||
});
|
||||
}
|
||||
|
||||
fn add_nested_code_sign_path(src_path: &Path, dest_path: &Path, sign_paths: &mut Vec<SignTarget>) {
|
||||
for folder_name in NESTED_CODE_FOLDER.iter() {
|
||||
let src_folder_path = src_path.join(folder_name);
|
||||
let dest_folder_path = dest_path.join(folder_name);
|
||||
|
||||
if src_folder_path.exists() {
|
||||
for entry in walkdir::WalkDir::new(src_folder_path)
|
||||
.min_depth(1)
|
||||
.max_depth(1)
|
||||
.into_iter()
|
||||
.filter_map(|e| e.ok())
|
||||
{
|
||||
if entry.path_is_symlink() || entry.file_name().to_string_lossy().starts_with('.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let dest_path = dest_folder_path.join(entry.file_name());
|
||||
let ext = entry.path().extension();
|
||||
if entry.path().is_dir() {
|
||||
// Bundles, like .app, .framework, .xpc
|
||||
if ext == Some(OsStr::new("framework")) {
|
||||
add_framework_sign_path(&entry.clone().into_path(), &dest_path, sign_paths);
|
||||
} else if ext == Some(OsStr::new("xpc")) || ext == Some(OsStr::new("app")) {
|
||||
add_executable_bundle_sign_path(&entry.clone().into_path(), &dest_path, sign_paths);
|
||||
}
|
||||
} else if entry.path().is_file() {
|
||||
// Binaries, like .dylib, Mach-O executables
|
||||
if ext == Some(OsStr::new("dylib")) {
|
||||
sign_paths.push(SignTarget {
|
||||
path: dest_path,
|
||||
is_an_executable: false,
|
||||
});
|
||||
} else if ext.is_none() {
|
||||
sign_paths.push(SignTarget {
|
||||
path: dest_path,
|
||||
is_an_executable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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)]);
|
||||
|
||||
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)]);
|
||||
|
||||
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.")
|
||||
);
|
||||
}
|
||||
}
|
||||
638
crates/tauri-bundler/src/bundle/macos/dmg/bundle_dmg
Normal file
638
crates/tauri-bundler/src/bundle/macos/dmg/bundle_dmg
Normal file
@@ -0,0 +1,638 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
# Create a read-only disk image of the contents of a folder
|
||||
# forked from https://github.com/create-dmg/create-dmg
|
||||
|
||||
# Bail out on any unhandled errors
|
||||
set -e;
|
||||
# Any command that exits with non-zero code will cause the pipeline to fail
|
||||
set -o pipefail;
|
||||
|
||||
CDMG_VERSION='1.2.1'
|
||||
|
||||
# The full path to the "support/" directory this script is using
|
||||
# (This will be set up by code later in the script.)
|
||||
CDMG_SUPPORT_DIR=""
|
||||
|
||||
OS_FULL_VERSION="$(sw_vers | sed -n 2p | cut -d : -f 2 | tr -d '[:space:]' | cut -c1-)"
|
||||
OS_MAJOR_VERSION="$(echo $OS_FULL_VERSION | cut -d . -f 1)"
|
||||
OS_MINOR_VERSION="$(echo $OS_FULL_VERSION | cut -d . -f 2)"
|
||||
WINX=10
|
||||
WINY=60
|
||||
WINW=500
|
||||
WINH=350
|
||||
ICON_SIZE=128
|
||||
TEXT_SIZE=16
|
||||
FORMAT="UDZO"
|
||||
FILESYSTEM="HFS+"
|
||||
ADD_FILE_SOURCES=()
|
||||
ADD_FILE_TARGETS=()
|
||||
IMAGEKEY=""
|
||||
HDIUTIL_VERBOSITY=""
|
||||
SANDBOX_SAFE=0
|
||||
BLESS=0
|
||||
SKIP_JENKINS=0
|
||||
MAXIMUM_UNMOUNTING_ATTEMPTS=3
|
||||
SIGNATURE=""
|
||||
NOTARIZE=""
|
||||
|
||||
function pure_version() {
|
||||
echo "$CDMG_VERSION"
|
||||
}
|
||||
|
||||
function hdiutil_detach_retry() {
|
||||
# Unmount
|
||||
unmounting_attempts=0
|
||||
until
|
||||
echo "Unmounting disk image..."
|
||||
(( unmounting_attempts++ ))
|
||||
hdiutil detach "$1"
|
||||
exit_code=$?
|
||||
(( exit_code == 0 )) && break # nothing goes wrong
|
||||
(( exit_code != 16 )) && exit $exit_code # exit with the original exit code
|
||||
# The above statement returns 1 if test failed (exit_code == 16).
|
||||
# It can make the code in the {do... done} block to be executed
|
||||
do
|
||||
(( unmounting_attempts == MAXIMUM_UNMOUNTING_ATTEMPTS )) && exit 16 # patience exhausted, exit with code EBUSY
|
||||
echo "Wait a moment..."
|
||||
sleep $(( 1 * (2 ** unmounting_attempts) ))
|
||||
done
|
||||
unset unmounting_attempts
|
||||
}
|
||||
|
||||
function version() {
|
||||
echo "create-dmg $(pure_version)"
|
||||
}
|
||||
|
||||
function usage() {
|
||||
version
|
||||
cat <<EOHELP
|
||||
|
||||
Creates a fancy DMG file.
|
||||
|
||||
Usage: $(basename $0) [options] <output_name.dmg> <source_folder>
|
||||
|
||||
All contents of <source_folder> will be copied into the disk image.
|
||||
|
||||
Options:
|
||||
--volname <name>
|
||||
set volume name (displayed in the Finder sidebar and window title)
|
||||
--volicon <icon.icns>
|
||||
set volume icon
|
||||
--background <pic.png>
|
||||
set folder background image (provide png, gif, or jpg)
|
||||
--window-pos <x> <y>
|
||||
set position the folder window
|
||||
--window-size <width> <height>
|
||||
set size of the folder window
|
||||
--text-size <text_size>
|
||||
set window text size (10-16)
|
||||
--icon-size <icon_size>
|
||||
set window icons size (up to 128)
|
||||
--icon file_name <x> <y>
|
||||
set position of the file's icon
|
||||
--hide-extension <file_name>
|
||||
hide the extension of file
|
||||
--app-drop-link <x> <y>
|
||||
make a drop link to Applications, at location x,y
|
||||
--ql-drop-link <x> <y>
|
||||
make a drop link to user QuickLook install dir, at location x,y
|
||||
--eula <eula_file>
|
||||
attach a license file to the dmg (plain text or RTF)
|
||||
--no-internet-enable
|
||||
disable automatic mount & copy
|
||||
--format <format>
|
||||
specify the final disk image format (UDZO|UDBZ|ULFO|ULMO) (default is UDZO)
|
||||
--filesystem <filesystem>
|
||||
specify the disk image filesystem (HFS+|APFS) (default is HFS+, APFS supports macOS 10.13 or newer)
|
||||
--encrypt
|
||||
enable encryption for the resulting disk image (AES-256 - you will be prompted for password)
|
||||
--encrypt-aes128
|
||||
enable encryption for the resulting disk image (AES-128 - you will be prompted for password)
|
||||
--add-file <target_name> <file>|<folder> <x> <y>
|
||||
add additional file or folder (can be used multiple times)
|
||||
--disk-image-size <x>
|
||||
set the disk image size manually to x MB
|
||||
--hdiutil-verbose
|
||||
execute hdiutil in verbose mode
|
||||
--hdiutil-quiet
|
||||
execute hdiutil in quiet mode
|
||||
--bless
|
||||
bless the mount folder (deprecated, needs macOS 12.2.1 or older)
|
||||
--codesign <signature>
|
||||
codesign the disk image with the specified signature
|
||||
--notarize <credentials>
|
||||
notarize the disk image (waits and staples) with the keychain stored credentials
|
||||
--sandbox-safe
|
||||
execute hdiutil with sandbox compatibility and do not bless (not supported for APFS disk images)
|
||||
--skip-jenkins
|
||||
skip Finder-prettifying AppleScript, useful in Sandbox and non-GUI environments
|
||||
--version
|
||||
show create-dmg version number
|
||||
-h, --help
|
||||
display this help screen
|
||||
|
||||
EOHELP
|
||||
exit 0
|
||||
}
|
||||
|
||||
# factors can cause interstitial disk images to contain more than a single
|
||||
# partition - expand the hunt for the temporary disk image by checking for
|
||||
# the path of the volume, versus assuming its the first result (as in pr/152).
|
||||
function find_mount_dir() {
|
||||
local dev_name="${1}"
|
||||
|
||||
>&2 echo "Searching for mounted interstitial disk image using ${dev_name}... "
|
||||
# enumerate up to 9 partitions
|
||||
for i in {1..9}; do
|
||||
# attempt to find the partition
|
||||
local found_dir
|
||||
found_dir=$(hdiutil info | grep -E --color=never "${dev_name}" | head -${i} | awk '{print $3}' | xargs)
|
||||
if [[ -n "${found_dir}" ]]; then
|
||||
echo "${found_dir}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Argument parsing
|
||||
|
||||
while [[ "${1:0:1}" = "-" ]]; do
|
||||
case $1 in
|
||||
--volname)
|
||||
VOLUME_NAME="$2"
|
||||
shift; shift;;
|
||||
--volicon)
|
||||
VOLUME_ICON_FILE="$2"
|
||||
shift; shift;;
|
||||
--background)
|
||||
BACKGROUND_FILE="$2"
|
||||
BACKGROUND_FILE_NAME="$(basename "$BACKGROUND_FILE")"
|
||||
BACKGROUND_CLAUSE="set background picture of opts to file \".background:$BACKGROUND_FILE_NAME\""
|
||||
REPOSITION_HIDDEN_FILES_CLAUSE="set position of every item to {theBottomRightX + 100, 100}"
|
||||
shift; shift;;
|
||||
--icon-size)
|
||||
ICON_SIZE="$2"
|
||||
shift; shift;;
|
||||
--text-size)
|
||||
TEXT_SIZE="$2"
|
||||
shift; shift;;
|
||||
--window-pos)
|
||||
WINX=$2; WINY=$3
|
||||
shift; shift; shift;;
|
||||
--window-size)
|
||||
WINW=$2; WINH=$3
|
||||
shift; shift; shift;;
|
||||
--icon)
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}set position of item \"$2\" to {$3, $4}
|
||||
"
|
||||
shift; shift; shift; shift;;
|
||||
--hide-extension)
|
||||
HIDING_CLAUSE="${HIDING_CLAUSE}set the extension hidden of item \"$2\" to true
|
||||
"
|
||||
shift; shift;;
|
||||
-h | --help)
|
||||
usage;;
|
||||
--version)
|
||||
version; exit 0;;
|
||||
--pure-version)
|
||||
pure_version; exit 0;;
|
||||
--ql-drop-link)
|
||||
QL_LINK=$2
|
||||
QL_CLAUSE="set position of item \"QuickLook\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--app-drop-link)
|
||||
APPLICATION_LINK=$2
|
||||
APPLICATION_CLAUSE="set position of item \"Applications\" to {$2, $3}
|
||||
"
|
||||
shift; shift; shift;;
|
||||
--eula)
|
||||
EULA_RSRC=$2
|
||||
shift; shift;;
|
||||
--no-internet-enable)
|
||||
NOINTERNET=1
|
||||
shift;;
|
||||
--format)
|
||||
FORMAT="$2"
|
||||
shift; shift;;
|
||||
--filesystem)
|
||||
FILESYSTEM="$2"
|
||||
shift; shift;;
|
||||
--encrypt)
|
||||
ENABLE_ENCRYPTION=1
|
||||
AESBITS=256
|
||||
shift;;
|
||||
--encrypt-aes128)
|
||||
ENABLE_ENCRYPTION=1
|
||||
AESBITS=128
|
||||
shift;;
|
||||
--add-file | --add-folder)
|
||||
ADD_FILE_TARGETS+=("$2")
|
||||
ADD_FILE_SOURCES+=("$3")
|
||||
POSITION_CLAUSE="${POSITION_CLAUSE}
|
||||
set position of item \"$2\" to {$4, $5}
|
||||
"
|
||||
shift; shift; shift; shift; shift;;
|
||||
--disk-image-size)
|
||||
DISK_IMAGE_SIZE="$2"
|
||||
shift; shift;;
|
||||
--hdiutil-verbose)
|
||||
HDIUTIL_VERBOSITY='-verbose'
|
||||
shift;;
|
||||
--hdiutil-quiet)
|
||||
HDIUTIL_VERBOSITY='-quiet'
|
||||
shift;;
|
||||
--codesign)
|
||||
SIGNATURE="$2"
|
||||
shift; shift;;
|
||||
--notarize)
|
||||
NOTARIZE="$2"
|
||||
shift; shift;;
|
||||
--sandbox-safe)
|
||||
SANDBOX_SAFE=1
|
||||
shift;;
|
||||
--bless)
|
||||
BLESS=1
|
||||
shift;;
|
||||
--rez)
|
||||
echo "REZ is no more directly used. You can remove the --rez argument."
|
||||
shift; shift;;
|
||||
--skip-jenkins)
|
||||
SKIP_JENKINS=1
|
||||
shift;;
|
||||
-*)
|
||||
echo "Unknown option: $1. Run 'create-dmg --help' for help."
|
||||
exit 1;;
|
||||
esac
|
||||
case $FORMAT in
|
||||
UDZO)
|
||||
IMAGEKEY="-imagekey zlib-level=9";;
|
||||
UDBZ)
|
||||
IMAGEKEY="-imagekey bzip2-level=9";;
|
||||
ULFO)
|
||||
;;
|
||||
ULMO)
|
||||
;;
|
||||
*)
|
||||
echo >&2 "Unknown disk image format: $FORMAT"
|
||||
exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$2" ]]; then
|
||||
echo "Not enough arguments. Run 'create-dmg --help' for help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DMG_PATH="$1"
|
||||
SRC_FOLDER="$(cd "$2" > /dev/null; pwd)"
|
||||
|
||||
# Argument validation checks
|
||||
|
||||
if [[ "${DMG_PATH: -4}" != ".dmg" ]]; then
|
||||
echo "Output file name must end with a .dmg extension. Run 'create-dmg --help' for help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${FILESYSTEM}" != "HFS+" ]] && [[ "${FILESYSTEM}" != "APFS" ]]; then
|
||||
echo "Unknown disk image filesystem: ${FILESYSTEM}. Run 'create-dmg --help' for help."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${FILESYSTEM}" == "APFS" ]] && [[ ${SANDBOX_SAFE} -eq 1 ]]; then
|
||||
echo "Creating an APFS disk image that is sandbox safe is not supported."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Main script logic
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
DMG_DIRNAME="$(dirname "$DMG_PATH")"
|
||||
DMG_DIR="$(cd "$DMG_DIRNAME" > /dev/null; pwd)"
|
||||
DMG_NAME="$(basename "$DMG_PATH")"
|
||||
DMG_TEMP_NAME="$DMG_DIR/rw.$$.${DMG_NAME}"
|
||||
|
||||
# Detect where we're running from
|
||||
|
||||
sentinel_file="$SCRIPT_DIR/.this-is-the-create-dmg-repo"
|
||||
if [[ -f "$sentinel_file" ]]; then
|
||||
# We're running from inside a repo
|
||||
CDMG_SUPPORT_DIR="$SCRIPT_DIR/support"
|
||||
else
|
||||
# We're running inside an installed location
|
||||
bin_dir="$SCRIPT_DIR"
|
||||
prefix_dir=$(dirname "$bin_dir")
|
||||
CDMG_SUPPORT_DIR="$prefix_dir/share/create-dmg/support"
|
||||
fi
|
||||
|
||||
if [[ -z "$VOLUME_NAME" ]]; then
|
||||
VOLUME_NAME="$(basename "$DMG_PATH" .dmg)"
|
||||
fi
|
||||
|
||||
if [[ ! -d "$CDMG_SUPPORT_DIR" ]]; then
|
||||
echo >&2 "Cannot find support/ directory: expected at: $CDMG_SUPPORT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -f "$SRC_FOLDER/.DS_Store" ]]; then
|
||||
echo "Deleting .DS_Store found in source folder"
|
||||
rm "$SRC_FOLDER/.DS_Store"
|
||||
fi
|
||||
|
||||
# Create the image
|
||||
echo "Creating disk image..."
|
||||
if [[ -f "${DMG_TEMP_NAME}" ]]; then
|
||||
rm -f "${DMG_TEMP_NAME}"
|
||||
fi
|
||||
|
||||
# Use Megabytes since hdiutil fails with very large byte numbers
|
||||
function blocks_to_megabytes() {
|
||||
# Add 1 extra MB, since there's no decimal retention here
|
||||
MB_SIZE=$((($1 * 512 / 1000 / 1000) + 1))
|
||||
echo $MB_SIZE
|
||||
}
|
||||
|
||||
function get_size() {
|
||||
# Get block size in disk
|
||||
if [[ $OS_MAJOR_VERSION -ge 12 ]]; then
|
||||
bytes_size=$(du -B 512 -s "$1")
|
||||
else
|
||||
bytes_size=$(du -s "$1")
|
||||
fi
|
||||
bytes_size=$(echo $bytes_size | sed -e 's/ .*//g')
|
||||
echo $(blocks_to_megabytes $bytes_size)
|
||||
}
|
||||
|
||||
# Create the DMG with the specified size or the hdiutil estimation
|
||||
CUSTOM_SIZE=''
|
||||
if [[ -n "$DISK_IMAGE_SIZE" ]]; then
|
||||
CUSTOM_SIZE="-size ${DISK_IMAGE_SIZE}m"
|
||||
fi
|
||||
|
||||
if [[ $SANDBOX_SAFE -eq 0 ]]; then
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
FILESYSTEM_ARGUMENTS=""
|
||||
else
|
||||
FILESYSTEM_ARGUMENTS="-c c=64,a=16,e=16"
|
||||
fi
|
||||
hdiutil create ${HDIUTIL_VERBOSITY} -srcfolder "$SRC_FOLDER" -volname "${VOLUME_NAME}" \
|
||||
-fs "${FILESYSTEM}" -fsargs "${FILESYSTEM_ARGUMENTS}" -format UDRW ${CUSTOM_SIZE} "${DMG_TEMP_NAME}"
|
||||
else
|
||||
hdiutil makehybrid ${HDIUTIL_VERBOSITY} -default-volume-name "${VOLUME_NAME}" -hfs -o "${DMG_TEMP_NAME}" "$SRC_FOLDER"
|
||||
hdiutil convert -format UDRW -ov -o "${DMG_TEMP_NAME}" "${DMG_TEMP_NAME}"
|
||||
DISK_IMAGE_SIZE_CUSTOM=$DISK_IMAGE_SIZE
|
||||
fi
|
||||
|
||||
# Get the created DMG actual size
|
||||
DISK_IMAGE_SIZE=$(get_size "${DMG_TEMP_NAME}")
|
||||
|
||||
# Use the custom size if bigger
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]] && [[ ! -z "$DISK_IMAGE_SIZE_CUSTOM" ]] && [[ $DISK_IMAGE_SIZE_CUSTOM -gt $DISK_IMAGE_SIZE ]]; then
|
||||
DISK_IMAGE_SIZE=$DISK_IMAGE_SIZE_CUSTOM
|
||||
fi
|
||||
|
||||
# Estimate the additional sources size
|
||||
if [[ -n "$ADD_FILE_SOURCES" ]]; then
|
||||
for i in "${!ADD_FILE_SOURCES[@]}"; do
|
||||
SOURCE_SIZE=$(get_size "${ADD_FILE_SOURCES[$i]}")
|
||||
DISK_IMAGE_SIZE=$(expr $DISK_IMAGE_SIZE + $SOURCE_SIZE)
|
||||
done
|
||||
fi
|
||||
|
||||
# Add extra space for additional resources
|
||||
DISK_IMAGE_SIZE=$(expr $DISK_IMAGE_SIZE + 20)
|
||||
|
||||
# Make sure target image size is within limits
|
||||
MIN_DISK_IMAGE_SIZE=$(hdiutil resize -limits "${DMG_TEMP_NAME}" | awk 'NR=1{print int($1/2048+1)}')
|
||||
if [ $MIN_DISK_IMAGE_SIZE -gt $DISK_IMAGE_SIZE ]; then
|
||||
DISK_IMAGE_SIZE=$MIN_DISK_IMAGE_SIZE
|
||||
fi
|
||||
|
||||
# Resize the image for the extra stuff
|
||||
hdiutil resize ${HDIUTIL_VERBOSITY} -size ${DISK_IMAGE_SIZE}m "${DMG_TEMP_NAME}"
|
||||
|
||||
# Mount the new DMG
|
||||
|
||||
echo "Mounting disk image..."
|
||||
|
||||
MOUNT_RANDOM_PATH="/Volumes"
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]]; then
|
||||
MOUNT_RANDOM_PATH="/tmp"
|
||||
fi
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
HDIUTIL_FILTER="tail -n 1"
|
||||
else
|
||||
HDIUTIL_FILTER="sed 1q"
|
||||
fi
|
||||
DEV_NAME=$(hdiutil attach -mountrandom ${MOUNT_RANDOM_PATH} -readwrite -noverify -noautoopen -nobrowse "${DMG_TEMP_NAME}" | grep -E --color=never '^/dev/' | ${HDIUTIL_FILTER} | awk '{print $1}')
|
||||
echo "Device name: $DEV_NAME"
|
||||
if [[ "$FILESYSTEM" == "APFS" ]]; then
|
||||
MOUNT_DIR=$(find_mount_dir "${DEV_NAME}")
|
||||
else
|
||||
MOUNT_DIR=$(find_mount_dir "${DEV_NAME}s")
|
||||
fi
|
||||
if [[ -z "${MOUNT_DIR}" ]]; then
|
||||
>&2 echo "ERROR: unable to proceed with final disk image creation because the interstitial disk image was not found."
|
||||
>&2 echo "The interstitial disk image will likely be mounted and will need to be cleaned up manually."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Mount dir: $MOUNT_DIR"
|
||||
|
||||
if [[ -n "$BACKGROUND_FILE" ]]; then
|
||||
echo "Copying background file '$BACKGROUND_FILE'..."
|
||||
[[ -d "$MOUNT_DIR/.background" ]] || mkdir "$MOUNT_DIR/.background"
|
||||
cp "$BACKGROUND_FILE" "$MOUNT_DIR/.background/$BACKGROUND_FILE_NAME"
|
||||
fi
|
||||
|
||||
if [[ -n "$APPLICATION_LINK" ]]; then
|
||||
echo "Making link to Applications dir..."
|
||||
echo $MOUNT_DIR
|
||||
ln -s /Applications "$MOUNT_DIR/Applications"
|
||||
fi
|
||||
|
||||
if [[ -n "$QL_LINK" ]]; then
|
||||
echo "Making link to QuickLook install dir..."
|
||||
echo $MOUNT_DIR
|
||||
ln -s "/Library/QuickLook" "$MOUNT_DIR/QuickLook"
|
||||
fi
|
||||
|
||||
if [[ -n "$VOLUME_ICON_FILE" ]]; then
|
||||
echo "Copying volume icon file '$VOLUME_ICON_FILE'..."
|
||||
cp "$VOLUME_ICON_FILE" "$MOUNT_DIR/.VolumeIcon.icns"
|
||||
SetFile -c icnC "$MOUNT_DIR/.VolumeIcon.icns"
|
||||
fi
|
||||
|
||||
if [[ -n "$ADD_FILE_SOURCES" ]]; then
|
||||
echo "Copying custom files..."
|
||||
for i in "${!ADD_FILE_SOURCES[@]}"; do
|
||||
echo "${ADD_FILE_SOURCES[$i]}"
|
||||
cp -a "${ADD_FILE_SOURCES[$i]}" "$MOUNT_DIR/${ADD_FILE_TARGETS[$i]}"
|
||||
done
|
||||
fi
|
||||
|
||||
VOLUME_NAME=$(basename $MOUNT_DIR)
|
||||
|
||||
# Run AppleScript to do all the Finder cosmetic stuff
|
||||
APPLESCRIPT_FILE=$(mktemp -t createdmg.tmp.XXXXXXXXXX)
|
||||
if [[ $SANDBOX_SAFE -eq 1 ]]; then
|
||||
echo "Skipping Finder-prettifying AppleScript because we are in Sandbox..."
|
||||
else
|
||||
if [[ $SKIP_JENKINS -eq 0 ]]; then
|
||||
cat "$CDMG_SUPPORT_DIR/template.applescript" \
|
||||
| sed -e "s/WINX/$WINX/g" -e "s/WINY/$WINY/g" -e "s/WINW/$WINW/g" \
|
||||
-e "s/WINH/$WINH/g" -e "s/BACKGROUND_CLAUSE/$BACKGROUND_CLAUSE/g" \
|
||||
-e "s/REPOSITION_HIDDEN_FILES_CLAUSE/$REPOSITION_HIDDEN_FILES_CLAUSE/g" \
|
||||
-e "s/ICON_SIZE/$ICON_SIZE/g" -e "s/TEXT_SIZE/$TEXT_SIZE/g" \
|
||||
| perl -pe "s/POSITION_CLAUSE/$POSITION_CLAUSE/g" \
|
||||
| perl -pe "s/QL_CLAUSE/$QL_CLAUSE/g" \
|
||||
| perl -pe "s/APPLICATION_CLAUSE/$APPLICATION_CLAUSE/g" \
|
||||
| perl -pe "s/HIDING_CLAUSE/$HIDING_CLAUSE/" \
|
||||
> "$APPLESCRIPT_FILE"
|
||||
|
||||
# pause to workaround occasional "Can’t get disk" (-1728) issues
|
||||
ERROR_1728_WORKAROUND_SLEEP_INTERVAL=2
|
||||
echo "Will sleep for $ERROR_1728_WORKAROUND_SLEEP_INTERVAL seconds to workaround occasions \"Can't get disk (-1728)\" issues..."
|
||||
sleep $ERROR_1728_WORKAROUND_SLEEP_INTERVAL
|
||||
|
||||
echo "Running AppleScript to make Finder stuff pretty: /usr/bin/osascript \"${APPLESCRIPT_FILE}\" \"${VOLUME_NAME}\""
|
||||
if /usr/bin/osascript "${APPLESCRIPT_FILE}" "${VOLUME_NAME}"; then
|
||||
# Okay, we're cool
|
||||
true
|
||||
else
|
||||
echo >&2 "Failed running AppleScript"
|
||||
hdiutil_detach_retry "${DEV_NAME}"
|
||||
exit 64
|
||||
fi
|
||||
echo "Done running the AppleScript..."
|
||||
sleep 4
|
||||
rm "$APPLESCRIPT_FILE"
|
||||
else
|
||||
echo ''
|
||||
echo "Will skip running AppleScript to configure DMG aesthetics because of --skip-jenkins option."
|
||||
echo "This will result in a DMG without any custom background or icons positioning."
|
||||
echo "More info at https://github.com/create-dmg/create-dmg/issues/72"
|
||||
echo ''
|
||||
fi
|
||||
fi
|
||||
|
||||
# Make sure it's not world writable
|
||||
echo "Fixing permissions..."
|
||||
chmod -Rf go-w "${MOUNT_DIR}" &> /dev/null || true
|
||||
echo "Done fixing permissions"
|
||||
|
||||
# Make the top window open itself on mount:
|
||||
if [[ $BLESS -eq 1 && $SANDBOX_SAFE -eq 0 ]]; then
|
||||
echo "Blessing started"
|
||||
if [ $(uname -m) == "arm64" ]; then
|
||||
bless --folder "${MOUNT_DIR}"
|
||||
else
|
||||
bless --folder "${MOUNT_DIR}" --openfolder "${MOUNT_DIR}"
|
||||
fi
|
||||
echo "Blessing finished"
|
||||
else
|
||||
echo "Skipping blessing on sandbox"
|
||||
fi
|
||||
|
||||
if [[ -n "$VOLUME_ICON_FILE" ]]; then
|
||||
# Tell the volume that it has a special file attribute
|
||||
SetFile -a C "$MOUNT_DIR"
|
||||
fi
|
||||
|
||||
# Delete unnecessary file system events log if possible
|
||||
echo "Deleting .fseventsd"
|
||||
rm -rf "${MOUNT_DIR}/.fseventsd" || true
|
||||
|
||||
hdiutil_detach_retry "${DEV_NAME}"
|
||||
|
||||
# Compress image and optionally encrypt
|
||||
if [[ $ENABLE_ENCRYPTION -eq 0 ]]; then
|
||||
echo "Compressing disk image..."
|
||||
hdiutil convert ${HDIUTIL_VERBOSITY} "${DMG_TEMP_NAME}" -format ${FORMAT} ${IMAGEKEY} -o "${DMG_DIR}/${DMG_NAME}"
|
||||
else
|
||||
echo "Compressing and encrypting disk image..."
|
||||
echo "NOTE: hdiutil will only prompt a single time for a password - ensure entry is correct."
|
||||
hdiutil convert ${HDIUTIL_VERBOSITY} "${DMG_TEMP_NAME}" -format ${FORMAT} ${IMAGEKEY} -encryption AES-${AESBITS} -stdinpass -o "${DMG_DIR}/${DMG_NAME}"
|
||||
fi
|
||||
rm -f "${DMG_TEMP_NAME}"
|
||||
|
||||
# Adding EULA resources
|
||||
if [[ -n "${EULA_RSRC}" && "${EULA_RSRC}" != "-null-" ]]; then
|
||||
echo "Adding EULA resources..."
|
||||
#
|
||||
# Use udifrez instead flatten/rez/unflatten
|
||||
# https://github.com/create-dmg/create-dmg/issues/109
|
||||
#
|
||||
# Based on a thread from dawn2dusk & peterguy
|
||||
# https://developer.apple.com/forums/thread/668084
|
||||
#
|
||||
EULA_RESOURCES_FILE=$(mktemp -t createdmg.tmp.XXXXXXXXXX)
|
||||
EULA_FORMAT=$(file -b ${EULA_RSRC})
|
||||
if [[ ${EULA_FORMAT} == 'Rich Text Format data'* ]] ; then
|
||||
EULA_FORMAT='RTF '
|
||||
else
|
||||
EULA_FORMAT='TEXT'
|
||||
fi
|
||||
# Encode the EULA to base64
|
||||
# Replace 'openssl base64' with 'base64' if Mac OS X 10.6 support is no more needed
|
||||
# EULA_DATA="$(base64 -b 52 "${EULA_RSRC}" | sed s$'/^\(.*\)$/\t\t\t\\1/')"
|
||||
EULA_DATA="$(openssl base64 -in "${EULA_RSRC}" | tr -d '\n' | awk '{gsub(/.{52}/,"&\n")}1' | sed s$'/^\(.*\)$/\t\t\t\\1/')"
|
||||
# Fill the template with the custom EULA contents
|
||||
eval "cat > \"${EULA_RESOURCES_FILE}\" <<EOF
|
||||
$(<${CDMG_SUPPORT_DIR}/eula-resources-template.xml)
|
||||
EOF
|
||||
"
|
||||
# Apply the resources
|
||||
hdiutil udifrez -xml "${EULA_RESOURCES_FILE}" '' -quiet "${DMG_DIR}/${DMG_NAME}" || {
|
||||
echo "Failed to add the EULA license"
|
||||
exit 1
|
||||
}
|
||||
echo "Successfully added the EULA license"
|
||||
fi
|
||||
|
||||
# Enable "internet", whatever that is
|
||||
if [[ ! -z "${NOINTERNET}" && "${NOINTERNET}" == 1 ]]; then
|
||||
echo "Not setting 'internet-enable' on the dmg, per caller request"
|
||||
else
|
||||
# Check if hdiutil supports internet-enable
|
||||
# Support was removed in macOS 10.15. See https://github.com/andreyvit/create-dmg/issues/76
|
||||
if hdiutil internet-enable -help >/dev/null 2>/dev/null; then
|
||||
hdiutil internet-enable -yes "${DMG_DIR}/${DMG_NAME}"
|
||||
else
|
||||
echo "hdiutil does not support internet-enable. Note it was removed in macOS 10.15."
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${SIGNATURE}" && "${SIGNATURE}" != "-null-" ]]; then
|
||||
echo "Codesign started"
|
||||
codesign -s "${SIGNATURE}" "${DMG_DIR}/${DMG_NAME}"
|
||||
dmgsignaturecheck="$(codesign --verify --deep --verbose=2 --strict "${DMG_DIR}/${DMG_NAME}" 2>&1 >/dev/null)"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "The disk image is now codesigned"
|
||||
else
|
||||
echo "The signature seems invalid${NC}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -n "${NOTARIZE}" && "${NOTARIZE}" != "-null-" ]]; then
|
||||
echo "Notarization started"
|
||||
xcrun notarytool submit "${DMG_DIR}/${DMG_NAME}" --keychain-profile "${NOTARIZE}" --wait
|
||||
echo "Stapling the notarization ticket"
|
||||
staple="$(xcrun stapler staple "${DMG_DIR}/${DMG_NAME}")"
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "The disk image is now notarized"
|
||||
else
|
||||
echo "$staple"
|
||||
echo "The notarization failed with error $?"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# All done!
|
||||
echo "Disk image done"
|
||||
exit 0
|
||||
@@ -0,0 +1,105 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LPic</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAAAAgAAAAAAAAAAAAQAAA==
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>STR#</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAYNRW5nbGlzaCB0ZXN0MQVBZ3JlZQhEaXNhZ3JlZQVQcmludAdT
|
||||
YXZlLi4ueklmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0
|
||||
aGlzIGxpY2Vuc2UsIGNsaWNrICJBZ3JlZSIgdG8gYWNjZXNzIHRo
|
||||
ZSBzb2Z0d2FyZS4gIElmIHlvdSBkbyBub3QgYWdyZWUsIHByZXNz
|
||||
ICJEaXNhZ3JlZS4i
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English buttons</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAYHRW5nbGlzaAVBZ3JlZQhEaXNhZ3JlZQVQcmludAdTYXZlLi4u
|
||||
e0lmIHlvdSBhZ3JlZSB3aXRoIHRoZSB0ZXJtcyBvZiB0aGlzIGxp
|
||||
Y2Vuc2UsIHByZXNzICJBZ3JlZSIgdG8gaW5zdGFsbCB0aGUgc29m
|
||||
dHdhcmUuICBJZiB5b3UgZG8gbm90IGFncmVlLCBwcmVzcyAiRGlz
|
||||
YWdyZWUiLg==
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5002</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>${EULA_FORMAT}</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
${EULA_DATA}
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>TMPL</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
E0RlZmF1bHQgTGFuZ3VhZ2UgSUREV1JEBUNvdW50T0NOVAQqKioq
|
||||
TFNUQwtzeXMgbGFuZyBJRERXUkQebG9jYWwgcmVzIElEIChvZmZz
|
||||
ZXQgZnJvbSA1MDAwRFdSRBAyLWJ5dGUgbGFuZ3VhZ2U/RFdSRAQq
|
||||
KioqTFNURQ==
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>128</string>
|
||||
<key>Name</key>
|
||||
<string>LPic</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>styl</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>Attributes</key>
|
||||
<string>0x0000</string>
|
||||
<key>Data</key>
|
||||
<data>
|
||||
AAMAAAAAAAwACQAUAAAAAAAAAAAAAAAAACcADAAJABQBAAAAAAAA
|
||||
AAAAAAAAKgAMAAkAFAAAAAAAAAAAAAA=
|
||||
</data>
|
||||
<key>ID</key>
|
||||
<string>5000</string>
|
||||
<key>Name</key>
|
||||
<string>English</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
213
crates/tauri-bundler/src/bundle/macos/dmg/mod.rs
Normal file
213
crates/tauri-bundler/src/bundle/macos/dmg/mod.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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
|
||||
|
||||
use super::{app, icon::create_icns_file};
|
||||
use crate::{
|
||||
PackageType, Settings,
|
||||
bundle::{Bundle, settings::Arch},
|
||||
error::{Context, ErrorExt},
|
||||
utils::CommandExt,
|
||||
};
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::{self, write},
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
pub struct Bundled {
|
||||
pub dmg: Vec<PathBuf>,
|
||||
pub app: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the DMG was created.
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Bundled> {
|
||||
// generate the .app bundle if needed
|
||||
let app_bundle_paths = if !bundles
|
||||
.iter()
|
||||
.any(|bundle| bundle.package_type == PackageType::MacOsBundle)
|
||||
{
|
||||
app::bundle_project(settings)?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// get the target path
|
||||
let output_path = settings.project_out_directory().join("bundle/dmg");
|
||||
let package_base_name = format!(
|
||||
"{}_{}_{}",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
match settings.binary_arch() {
|
||||
Arch::X86_64 => "x64",
|
||||
Arch::AArch64 => "aarch64",
|
||||
Arch::Universal => "universal",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"Unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
);
|
||||
let dmg_name = format!("{package_base_name}.dmg");
|
||||
let dmg_path = output_path.join(&dmg_name);
|
||||
|
||||
let product_name = settings.product_name();
|
||||
let bundle_file_name = format!("{product_name}.app");
|
||||
let bundle_dir = settings.project_out_directory().join("bundle/macos");
|
||||
|
||||
let support_directory_path = output_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("share/create-dmg/support");
|
||||
|
||||
for path in &[&support_directory_path, &output_path] {
|
||||
if path.exists() {
|
||||
fs::remove_dir_all(path).fs_context("failed to remove old dmg", path.to_path_buf())?;
|
||||
}
|
||||
fs::create_dir_all(path).fs_context("failed to create output directory", path.to_path_buf())?;
|
||||
}
|
||||
|
||||
// create paths for script
|
||||
let bundle_script_path = output_path.join("bundle_dmg.sh");
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", dmg_name, dmg_path.display());
|
||||
|
||||
// write the scripts
|
||||
write(&bundle_script_path, include_str!("./bundle_dmg"))?;
|
||||
write(
|
||||
support_directory_path.join("template.applescript"),
|
||||
include_str!("./template.applescript"),
|
||||
)?;
|
||||
write(
|
||||
support_directory_path.join("eula-resources-template.xml"),
|
||||
include_str!("./eula-resources-template.xml"),
|
||||
)?;
|
||||
|
||||
// chmod script for execution
|
||||
Command::new("chmod")
|
||||
.arg("777")
|
||||
.arg(&bundle_script_path)
|
||||
.current_dir(&output_path)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("Failed to chmod script");
|
||||
|
||||
let dmg_settings = settings.dmg();
|
||||
|
||||
let app_position = &dmg_settings.app_position;
|
||||
let application_folder_position = &dmg_settings.application_folder_position;
|
||||
let window_size = &dmg_settings.window_size;
|
||||
|
||||
let app_position_x = app_position.x.to_string();
|
||||
let app_position_y = app_position.y.to_string();
|
||||
let application_folder_position_x = application_folder_position.x.to_string();
|
||||
let application_folder_position_y = application_folder_position.y.to_string();
|
||||
let window_size_width = window_size.width.to_string();
|
||||
let window_size_height = window_size.height.to_string();
|
||||
|
||||
let mut bundle_dmg_cmd = Command::new(&bundle_script_path);
|
||||
|
||||
bundle_dmg_cmd.args([
|
||||
"--volname",
|
||||
product_name,
|
||||
"--icon",
|
||||
&bundle_file_name,
|
||||
&app_position_x,
|
||||
&app_position_y,
|
||||
"--app-drop-link",
|
||||
&application_folder_position_x,
|
||||
&application_folder_position_y,
|
||||
"--window-size",
|
||||
&window_size_width,
|
||||
&window_size_height,
|
||||
"--hide-extension",
|
||||
&bundle_file_name,
|
||||
]);
|
||||
|
||||
let window_position = dmg_settings
|
||||
.window_position
|
||||
.as_ref()
|
||||
.map(|position| (position.x.to_string(), position.y.to_string()));
|
||||
|
||||
if let Some(window_position) = &window_position {
|
||||
bundle_dmg_cmd.arg("--window-pos");
|
||||
bundle_dmg_cmd.arg(&window_position.0);
|
||||
bundle_dmg_cmd.arg(&window_position.1);
|
||||
}
|
||||
|
||||
let background_path = if let Some(background_path) = &dmg_settings.background {
|
||||
Some(env::current_dir()?.join(background_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(background_path) = &background_path {
|
||||
bundle_dmg_cmd.arg("--background");
|
||||
bundle_dmg_cmd.arg(background_path);
|
||||
}
|
||||
|
||||
let icns_icon_path = create_icns_file(&output_path, settings)?;
|
||||
if let Some(icon) = &icns_icon_path {
|
||||
bundle_dmg_cmd.arg("--volicon");
|
||||
bundle_dmg_cmd.arg(icon);
|
||||
}
|
||||
|
||||
let license_path = if let Some(license_path) = settings.license_file() {
|
||||
Some(env::current_dir()?.join(license_path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(license_path) = &license_path {
|
||||
bundle_dmg_cmd.arg("--eula");
|
||||
bundle_dmg_cmd.arg(license_path);
|
||||
}
|
||||
|
||||
// Issue #592 - Building MacOS dmg files on CI
|
||||
// https://github.com/tauri-apps/tauri/issues/592
|
||||
if env::var_os("TAURI_BUNDLER_DMG_IGNORE_CI").unwrap_or_default() != "true" {
|
||||
if let Some(value) = env::var_os("CI") {
|
||||
if value == "true" {
|
||||
bundle_dmg_cmd.arg("--skip-jenkins");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(action = "Running"; "bundle_dmg.sh");
|
||||
|
||||
// execute the bundle script
|
||||
bundle_dmg_cmd
|
||||
.current_dir(bundle_dir.clone())
|
||||
.args(vec![dmg_name.as_str(), bundle_file_name.as_str()])
|
||||
.output_ok()
|
||||
.context("error running bundle_dmg.sh")?;
|
||||
|
||||
fs::rename(bundle_dir.join(dmg_name), dmg_path.clone())?;
|
||||
|
||||
// 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 !settings.no_sign() && identity != Some("-") {
|
||||
if let Some(keychain) = super::sign::keychain(identity)? {
|
||||
super::sign::sign(
|
||||
&keychain,
|
||||
vec![super::sign::SignTarget {
|
||||
path: dmg_path.clone(),
|
||||
is_an_executable: false,
|
||||
}],
|
||||
settings,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Bundled {
|
||||
dmg: vec![dmg_path],
|
||||
app: app_bundle_paths,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
on run (volumeName)
|
||||
tell application "Finder"
|
||||
tell disk (volumeName as string)
|
||||
open
|
||||
|
||||
set theXOrigin to WINX
|
||||
set theYOrigin to WINY
|
||||
set theWidth to WINW
|
||||
set theHeight to WINH
|
||||
|
||||
set theBottomRightX to (theXOrigin + theWidth)
|
||||
set theBottomRightY to (theYOrigin + theHeight)
|
||||
set dsStore to "\"" & "/Volumes/" & volumeName & "/" & ".DS_STORE\""
|
||||
|
||||
tell container window
|
||||
set current view to icon view
|
||||
set toolbar visible to false
|
||||
set statusbar visible to false
|
||||
set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
|
||||
set statusbar visible to false
|
||||
REPOSITION_HIDDEN_FILES_CLAUSE
|
||||
end tell
|
||||
|
||||
set opts to the icon view options of container window
|
||||
tell opts
|
||||
set icon size to ICON_SIZE
|
||||
set text size to TEXT_SIZE
|
||||
set arrangement to not arranged
|
||||
end tell
|
||||
BACKGROUND_CLAUSE
|
||||
|
||||
-- Positioning
|
||||
POSITION_CLAUSE
|
||||
|
||||
-- Hiding
|
||||
HIDING_CLAUSE
|
||||
|
||||
-- Application and QL Link Clauses
|
||||
APPLICATION_CLAUSE
|
||||
QL_CLAUSE
|
||||
close
|
||||
open
|
||||
-- Force saving of the size
|
||||
delay 1
|
||||
|
||||
tell container window
|
||||
set statusbar visible to false
|
||||
set the bounds to {theXOrigin, theYOrigin, theBottomRightX - 10, theBottomRightY - 10}
|
||||
end tell
|
||||
end tell
|
||||
|
||||
delay 1
|
||||
|
||||
tell disk (volumeName as string)
|
||||
tell container window
|
||||
set statusbar visible to false
|
||||
set the bounds to {theXOrigin, theYOrigin, theBottomRightX, theBottomRightY}
|
||||
end tell
|
||||
end tell
|
||||
|
||||
--give the finder some time to write the .DS_Store file
|
||||
delay 3
|
||||
|
||||
set waitTime to 0
|
||||
set ejectMe to false
|
||||
repeat while ejectMe is false
|
||||
delay 1
|
||||
set waitTime to waitTime + 1
|
||||
|
||||
if (do shell script "[ -f " & dsStore & " ]; echo $?") = "0" then set ejectMe to true
|
||||
end repeat
|
||||
log "waited " & waitTime & " seconds for .DS_STORE to be created."
|
||||
end tell
|
||||
end run
|
||||
326
crates/tauri-bundler/src/bundle/macos/icon.rs
Normal file
326
crates/tauri-bundler/src/bundle/macos/icon.rs
Normal file
@@ -0,0 +1,326 @@
|
||||
// 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
|
||||
|
||||
use crate::bundle::Settings;
|
||||
use crate::utils::{self, CommandExt, fs_utils};
|
||||
use std::{
|
||||
cmp::min,
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter},
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use image::GenericImageView;
|
||||
|
||||
// Given a list of icon files, try to produce an ICNS file in the out_dir
|
||||
// and return the path to it. Returns `Ok(None)` if no usable icons
|
||||
// were provided.
|
||||
pub fn create_icns_file(out_dir: &Path, settings: &Settings) -> crate::Result<Option<PathBuf>> {
|
||||
if settings.icon_files().count() == 0 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If one of the icon files is already an ICNS file, just use that.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() == Some(OsStr::new("icns")) {
|
||||
let mut dest_path = out_dir.to_path_buf();
|
||||
dest_path.push(icon_path.file_name().expect("Could not get icon filename"));
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
return Ok(Some(dest_path));
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, read available images and pack them into a new ICNS file.
|
||||
let mut family = icns::IconFamily::new();
|
||||
|
||||
fn add_icon_to_family(
|
||||
icon: image::DynamicImage,
|
||||
density: u32,
|
||||
family: &mut icns::IconFamily,
|
||||
) -> io::Result<()> {
|
||||
// Try to add this image to the icon family. Ignore images whose sizes
|
||||
// don't map to any ICNS icon type; print warnings and skip images that
|
||||
// fail to encode.
|
||||
match icns::IconType::from_pixel_size_and_density(icon.width(), icon.height(), density) {
|
||||
Some(icon_type) => {
|
||||
if !family.has_icon_with_type(icon_type) {
|
||||
let icon = make_icns_image(icon)?;
|
||||
family.add_icon_with_type(&icon, icon_type)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
None => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"No matching IconType",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
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().is_some_and(|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();
|
||||
let orig_size = min(w, h);
|
||||
let next_size_down = 2f32.powf((orig_size as f32).log2().floor()) as u32;
|
||||
if orig_size > next_size_down {
|
||||
images_to_resize.push((icon, next_size_down, density));
|
||||
} else {
|
||||
add_icon_to_family(icon, density, &mut family)?;
|
||||
}
|
||||
}
|
||||
|
||||
for (icon, next_size_down, density) in images_to_resize {
|
||||
let icon = icon.resize_exact(
|
||||
next_size_down,
|
||||
next_size_down,
|
||||
image::imageops::FilterType::Lanczos3,
|
||||
);
|
||||
add_icon_to_family(icon, density, &mut family)?;
|
||||
}
|
||||
|
||||
if !family.is_empty() {
|
||||
fs::create_dir_all(out_dir)?;
|
||||
let mut dest_path = out_dir.to_path_buf();
|
||||
dest_path.push(settings.product_name());
|
||||
dest_path.set_extension("icns");
|
||||
let icns_file = BufWriter::new(File::create(&dest_path)?);
|
||||
family.write(icns_file)?;
|
||||
Ok(Some(dest_path))
|
||||
} else {
|
||||
Err(crate::Error::GenericError(
|
||||
"No usable Icon files found".to_owned(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Converts an image::DynamicImage into an icns::Image.
|
||||
fn make_icns_image(img: image::DynamicImage) -> io::Result<icns::Image> {
|
||||
let pixel_format = match img.color() {
|
||||
image::ColorType::Rgba8 => icns::PixelFormat::RGBA,
|
||||
image::ColorType::Rgb8 => icns::PixelFormat::RGB,
|
||||
image::ColorType::La8 => icns::PixelFormat::GrayAlpha,
|
||||
image::ColorType::L8 => icns::PixelFormat::Gray,
|
||||
_ => {
|
||||
let msg = format!("unsupported ColorType: {:?}", img.color());
|
||||
return Err(io::Error::new(io::ErrorKind::InvalidData, msg));
|
||||
}
|
||||
};
|
||||
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());
|
||||
}
|
||||
}
|
||||
211
crates/tauri-bundler/src/bundle/macos/ios.rs
Normal file
211
crates/tauri-bundler/src/bundle/macos/ios.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
// 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
|
||||
|
||||
// An iOS package is laid out like:
|
||||
//
|
||||
// Foobar.app # Actually a directory
|
||||
// Foobar # The main binary executable of the app
|
||||
// Info.plist # An XML file containing the app's metadata
|
||||
// ... # Icons and other resource files
|
||||
//
|
||||
// See https://developer.apple.com/go/?id=bundle-structure for a full
|
||||
// explanation.
|
||||
|
||||
use crate::{
|
||||
Settings,
|
||||
error::{Context, ErrorExt},
|
||||
utils::{self, fs_utils},
|
||||
};
|
||||
|
||||
use image::{GenericImageView, ImageDecoder, codecs::png::PngDecoder};
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
ffi::OsStr,
|
||||
fs::{self, File},
|
||||
io::{BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
/// Bundles the project.
|
||||
/// Returns a vector of PathBuf that shows where the .app was created.
|
||||
pub fn bundle_project(settings: &Settings) -> crate::Result<Vec<PathBuf>> {
|
||||
log::warn!("iOS bundle support is still experimental.");
|
||||
|
||||
let app_product_name = format!("{}.app", settings.product_name());
|
||||
|
||||
let app_bundle_path = settings
|
||||
.project_out_directory()
|
||||
.join("bundle/ios")
|
||||
.join(&app_product_name);
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", app_product_name, app_bundle_path.display());
|
||||
|
||||
if app_bundle_path.exists() {
|
||||
fs::remove_dir_all(&app_bundle_path)
|
||||
.fs_context("failed to remove old app bundle", &app_bundle_path)?;
|
||||
}
|
||||
fs::create_dir_all(&app_bundle_path)
|
||||
.fs_context("failed to create bundle directory", &app_bundle_path)?;
|
||||
|
||||
for src in settings.resource_files() {
|
||||
let src = src?;
|
||||
let dest = app_bundle_path.join(tauri_utils::resources::resource_relpath(&src));
|
||||
fs_utils::copy_file(&src, &dest)
|
||||
.with_context(|| format!("Failed to copy resource file {src:?}"))?;
|
||||
}
|
||||
|
||||
let icon_filenames = generate_icon_files(&app_bundle_path, settings)
|
||||
.with_context(|| "Failed to create app icons")?;
|
||||
generate_info_plist(&app_bundle_path, settings, &icon_filenames)
|
||||
.with_context(|| "Failed to create Info.plist")?;
|
||||
|
||||
for bin in settings.binaries() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
fs_utils::copy_file(&bin_path, &app_bundle_path.join(bin.name()))
|
||||
.with_context(|| format!("Failed to copy binary from {bin_path:?}"))?;
|
||||
}
|
||||
|
||||
Ok(vec![app_bundle_path])
|
||||
}
|
||||
|
||||
/// Generate the icon files and store them under the `bundle_dir`.
|
||||
fn generate_icon_files(bundle_dir: &Path, settings: &Settings) -> crate::Result<Vec<String>> {
|
||||
let mut filenames = Vec::new();
|
||||
{
|
||||
let mut get_dest_path = |width: u32, height: u32, is_retina: bool| {
|
||||
let filename = format!(
|
||||
"icon_{}x{}{}.png",
|
||||
width,
|
||||
height,
|
||||
if is_retina { "@2x" } else { "" }
|
||||
);
|
||||
let path = bundle_dir.join(&filename);
|
||||
filenames.push(filename);
|
||||
path
|
||||
};
|
||||
let mut sizes = BTreeSet::new();
|
||||
// Prefer PNG files.
|
||||
for icon_path in settings.icon_files() {
|
||||
let icon_path = icon_path?;
|
||||
if icon_path.extension() != Some(OsStr::new("png")) {
|
||||
continue;
|
||||
}
|
||||
let decoder = PngDecoder::new(BufReader::new(File::open(&icon_path)?))?;
|
||||
let width = decoder.dimensions().0;
|
||||
let height = decoder.dimensions().1;
|
||||
let is_retina = utils::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
fs_utils::copy_file(&icon_path, &dest_path)?;
|
||||
}
|
||||
}
|
||||
// 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()
|
||||
.is_some_and(|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)?)?;
|
||||
for icon_type in icon_family.available_icons() {
|
||||
let width = icon_type.screen_width();
|
||||
let height = icon_type.screen_height();
|
||||
let is_retina = icon_type.pixel_density() > 1;
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
let icon = icon_family.get_icon_with_type(icon_type)?;
|
||||
icon.write_png(File::create(dest_path)?)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let icon = image::open(&icon_path)?;
|
||||
let (width, height) = icon.dimensions();
|
||||
let is_retina = utils::is_retina(&icon_path);
|
||||
if !sizes.contains(&(width, height, is_retina)) {
|
||||
sizes.insert((width, height, is_retina));
|
||||
let dest_path = get_dest_path(width, height, is_retina);
|
||||
icon.write_to(
|
||||
&mut fs_utils::create_file(&dest_path)?,
|
||||
image::ImageFormat::Png,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(filenames)
|
||||
}
|
||||
|
||||
/// Generates the Info.plist file
|
||||
fn generate_info_plist(
|
||||
bundle_dir: &Path,
|
||||
settings: &Settings,
|
||||
icon_filenames: &[String],
|
||||
) -> crate::Result<()> {
|
||||
let file = &mut fs_utils::create_file(&bundle_dir.join("Info.plist"))?;
|
||||
writeln!(
|
||||
file,
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
|
||||
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\" \
|
||||
\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n\
|
||||
<plist version=\"1.0\">\n\
|
||||
<dict>"
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleIdentifier</key>\n <string>{}</string>",
|
||||
settings.bundle_identifier()
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleDisplayName</key>\n <string>{}</string>",
|
||||
settings.product_name()
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleName</key>\n <string>{}</string>",
|
||||
settings.product_name()
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleExecutable</key>\n <string>{}</string>",
|
||||
settings.main_binary_name()?
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleVersion</key>\n <string>{}</string>",
|
||||
settings
|
||||
.ios()
|
||||
.bundle_version
|
||||
.as_deref()
|
||||
.unwrap_or_else(|| settings.version_string())
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleShortVersionString</key>\n <string>{}</string>",
|
||||
settings.version_string()
|
||||
)?;
|
||||
writeln!(
|
||||
file,
|
||||
" <key>CFBundleDevelopmentRegion</key>\n <string>en_US</string>"
|
||||
)?;
|
||||
|
||||
if !icon_filenames.is_empty() {
|
||||
writeln!(file, " <key>CFBundleIconFiles</key>\n <array>")?;
|
||||
for filename in icon_filenames {
|
||||
writeln!(file, " <string>{filename}</string>")?;
|
||||
}
|
||||
writeln!(file, " </array>")?;
|
||||
}
|
||||
writeln!(file, " <key>LSRequiresIPhoneOS</key>\n <true/>")?;
|
||||
writeln!(file, "</dict>\n</plist>")?;
|
||||
file.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
10
crates/tauri-bundler/src/bundle/macos/mod.rs
Normal file
10
crates/tauri-bundler/src/bundle/macos/mod.rs
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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
|
||||
|
||||
pub mod app;
|
||||
pub mod dmg;
|
||||
pub mod icon;
|
||||
pub mod ios;
|
||||
pub mod sign;
|
||||
165
crates/tauri-bundler/src/bundle/macos/sign.rs
Normal file
165
crates/tauri-bundler/src/bundle/macos/sign.rs
Normal file
@@ -0,0 +1,165 @@
|
||||
// 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
|
||||
|
||||
use std::{
|
||||
env::{var, var_os},
|
||||
ffi::OsString,
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::{Entitlements, Settings, error::NotarizeAuthError};
|
||||
|
||||
pub struct SignTarget {
|
||||
pub path: PathBuf,
|
||||
pub is_an_executable: bool,
|
||||
}
|
||||
|
||||
pub fn keychain(identity: Option<&str>) -> crate::Result<Option<tauri_macos_sign::Keychain>> {
|
||||
if let (Some(certificate_encoded), Some(certificate_password)) = (
|
||||
var_os("APPLE_CERTIFICATE"),
|
||||
var_os("APPLE_CERTIFICATE_PASSWORD"),
|
||||
) {
|
||||
// import user certificate - useful for CI build
|
||||
let keychain =
|
||||
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(crate::Error::GenericError(format!(
|
||||
"certificate from APPLE_CERTIFICATE \"{certificate_identity}\" environment variable does not match provided identity \"{identity}\""
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(Some(keychain))
|
||||
} else if let Some(identity) = identity {
|
||||
Ok(Some(tauri_macos_sign::Keychain::with_signing_identity(
|
||||
identity,
|
||||
)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign(
|
||||
keychain: &tauri_macos_sign::Keychain,
|
||||
targets: Vec<SignTarget>,
|
||||
settings: &Settings,
|
||||
) -> crate::Result<()> {
|
||||
log::info!(action = "Signing"; "with identity \"{}\"", keychain.signing_identity());
|
||||
|
||||
for target in targets {
|
||||
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.as_deref(),
|
||||
target.is_an_executable && settings.macos().hardened_runtime,
|
||||
)
|
||||
.map_err(Box::new)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn notarize(
|
||||
keychain: &tauri_macos_sign::Keychain,
|
||||
app_bundle_path: PathBuf,
|
||||
credentials: &tauri_macos_sign::AppleNotarizationCredentials,
|
||||
) -> crate::Result<()> {
|
||||
tauri_macos_sign::notarize(keychain, &app_bundle_path, credentials)
|
||||
.map_err(Box::new)
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
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>
|
||||
{
|
||||
match (
|
||||
var_os("APPLE_ID"),
|
||||
var_os("APPLE_PASSWORD"),
|
||||
var_os("APPLE_TEAM_ID"),
|
||||
) {
|
||||
(Some(apple_id), Some(password), Some(team_id)) => {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::AppleId {
|
||||
apple_id,
|
||||
password,
|
||||
team_id,
|
||||
})
|
||||
}
|
||||
(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"),
|
||||
) {
|
||||
(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,
|
||||
})
|
||||
}
|
||||
(Some(key_id), Some(issuer), Err(_)) => {
|
||||
let mut api_key_file_name = OsString::from("AuthKey_");
|
||||
api_key_file_name.push(&key_id);
|
||||
api_key_file_name.push(".p8");
|
||||
let mut key_path = None;
|
||||
|
||||
let mut search_paths = vec!["./private_keys".into()];
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
search_paths.push(home_dir.join("private_keys"));
|
||||
search_paths.push(home_dir.join(".private_keys"));
|
||||
search_paths.push(home_dir.join(".appstoreconnect").join("private_keys"));
|
||||
}
|
||||
|
||||
for folder in search_paths {
|
||||
if let Some(path) = find_api_key(folder, &api_key_file_name) {
|
||||
key_path = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(key_path) = key_path {
|
||||
Ok(tauri_macos_sign::AppleNotarizationCredentials::ApiKey {
|
||||
key_id,
|
||||
key: tauri_macos_sign::ApiKey::Path(key_path),
|
||||
issuer,
|
||||
})
|
||||
} else {
|
||||
Err(NotarizeAuthError::MissingApiKey {
|
||||
file_name: api_key_file_name.to_string_lossy().into_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
_ => Err(NotarizeAuthError::MissingCredentials),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_api_key(folder: PathBuf, file_name: &OsString) -> Option<PathBuf> {
|
||||
let path = folder.join(file_name);
|
||||
if path.exists() { Some(path) } else { None }
|
||||
}
|
||||
134
crates/tauri-bundler/src/bundle/platform.rs
Normal file
134
crates/tauri-bundler/src/bundle/platform.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
// 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
|
||||
|
||||
use crate::utils::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
struct RustCfg {
|
||||
target_arch: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_rust_cfg(cfg: String) -> RustCfg {
|
||||
let target_line = "target_arch=\"";
|
||||
let mut target_arch = None;
|
||||
for line in cfg.split('\n') {
|
||||
if line.starts_with(target_line) {
|
||||
let len = target_line.len();
|
||||
let arch = line.chars().skip(len).take(line.len() - len - 1).collect();
|
||||
target_arch.replace(arch);
|
||||
}
|
||||
}
|
||||
RustCfg { target_arch }
|
||||
}
|
||||
|
||||
/// Try to determine the current target triple.
|
||||
///
|
||||
/// Returns a target triple (e.g. `x86_64-unknown-linux-gnu` or `i686-pc-windows-msvc`) or an
|
||||
/// `Error::Config` if the current config cannot be determined or is not some combination of the
|
||||
/// following values:
|
||||
/// `linux, mac, windows` -- `i686, x86, armv7` -- `gnu, musl, msvc`
|
||||
///
|
||||
/// * Errors:
|
||||
/// * Unexpected system config
|
||||
pub fn target_triple() -> Result<String, crate::Error> {
|
||||
let arch_res = Command::new("rustc").args(["--print", "cfg"]).output_ok();
|
||||
|
||||
let arch = match arch_res {
|
||||
Ok(output) => parse_rust_cfg(String::from_utf8_lossy(&output.stdout).into())
|
||||
.target_arch
|
||||
.expect("could not find `target_arch` when running `rustc --print cfg`."),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to determine target arch using rustc, error: `{}`. The fallback is the architecture of the machine that compiled this crate.",
|
||||
err,
|
||||
);
|
||||
if cfg!(target_arch = "x86") {
|
||||
"i686".into()
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x86_64".into()
|
||||
} else if cfg!(target_arch = "arm") {
|
||||
"armv7".into()
|
||||
} else if cfg!(target_arch = "aarch64") {
|
||||
"aarch64".into()
|
||||
} else if cfg!(target_arch = "riscv64") {
|
||||
"riscv64".into()
|
||||
} else {
|
||||
return Err(crate::Error::ArchError(String::from(
|
||||
"Unable to determine target-architecture",
|
||||
)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "linux") {
|
||||
"unknown-linux"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"apple-darwin"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"pc-windows"
|
||||
} else if cfg!(target_os = "freebsd") {
|
||||
"unknown-freebsd"
|
||||
} else {
|
||||
return Err(crate::Error::ArchError(String::from(
|
||||
"Unable to determine target-os",
|
||||
)));
|
||||
};
|
||||
|
||||
let os = if cfg!(target_os = "macos") || cfg!(target_os = "freebsd") {
|
||||
String::from(os)
|
||||
} else {
|
||||
let env = if cfg!(target_env = "gnu") {
|
||||
"gnu"
|
||||
} else if cfg!(target_env = "musl") {
|
||||
"musl"
|
||||
} else if cfg!(target_env = "msvc") {
|
||||
"msvc"
|
||||
} else {
|
||||
return Err(crate::Error::ArchError(String::from(
|
||||
"Unable to determine target-environment",
|
||||
)));
|
||||
};
|
||||
|
||||
format!("{os}-{env}")
|
||||
};
|
||||
|
||||
Ok(format!("{arch}-{os}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::RustCfg;
|
||||
|
||||
#[test]
|
||||
fn parse_rust_cfg() {
|
||||
assert_eq!(
|
||||
super::parse_rust_cfg("target_arch".into()),
|
||||
RustCfg { target_arch: None }
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::parse_rust_cfg(
|
||||
r#"debug_assertions
|
||||
target_arch="aarch64"
|
||||
target_endian="little"
|
||||
target_env=""
|
||||
target_family="unix"
|
||||
target_os="macos"
|
||||
target_pointer_width="64"
|
||||
target_vendor="apple"
|
||||
unix"#
|
||||
.into()
|
||||
),
|
||||
RustCfg {
|
||||
target_arch: Some("aarch64".into())
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
1369
crates/tauri-bundler/src/bundle/settings.rs
Normal file
1369
crates/tauri-bundler/src/bundle/settings.rs
Normal file
File diff suppressed because it is too large
Load Diff
291
crates/tauri-bundler/src/bundle/updater_bundle.rs
Normal file
291
crates/tauri-bundler/src/bundle/updater_bundle.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
// 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
|
||||
|
||||
use crate::{
|
||||
Settings,
|
||||
bundle::{
|
||||
Bundle,
|
||||
windows::{
|
||||
NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME,
|
||||
WIX_UPDATER_OUTPUT_FOLDER_NAME,
|
||||
},
|
||||
},
|
||||
error::{Context, ErrorExt},
|
||||
utils::fs_utils,
|
||||
};
|
||||
use tauri_utils::{display_path, platform::Target as TargetPlatform};
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::prelude::*,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
// Build update
|
||||
pub fn bundle_project(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
let target_os = settings.target_platform();
|
||||
|
||||
if matches!(target_os, TargetPlatform::Windows) {
|
||||
return bundle_update_windows(settings, bundles);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
return bundle_update_macos(bundles);
|
||||
#[cfg(target_os = "linux")]
|
||||
return bundle_update_linux(bundles);
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
{
|
||||
log::error!("Current platform does not support updates");
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
// Create simple update-macos.tar.gz
|
||||
// This is the Mac OS App packaged
|
||||
#[cfg(target_os = "macos")]
|
||||
fn bundle_update_macos(bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
use std::ffi::OsStr;
|
||||
|
||||
// find our .app or rebuild our bundle
|
||||
if let Some(source_path) = bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == crate::PackageType::MacOsBundle)
|
||||
.find_map(|bundle| {
|
||||
bundle
|
||||
.bundle_paths
|
||||
.iter()
|
||||
.find(|path| path.extension() == Some(OsStr::new("app")))
|
||||
})
|
||||
{
|
||||
// add .tar.gz to our path
|
||||
let osx_archived = format!("{}.tar.gz", source_path.display());
|
||||
let osx_archived_path = PathBuf::from(&osx_archived);
|
||||
|
||||
// Create our gzip file (need to send parent)
|
||||
// as we walk the source directory (source isnt added)
|
||||
create_tar(source_path, &osx_archived_path)
|
||||
.with_context(|| "Failed to tar.gz update directory")?;
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", osx_archived, display_path(&osx_archived_path));
|
||||
|
||||
Ok(vec![osx_archived_path])
|
||||
} else {
|
||||
Err(crate::Error::UnableToFindProject)
|
||||
}
|
||||
}
|
||||
|
||||
// Create simple update-linux_<arch>.tar.gz
|
||||
// Including the AppImage
|
||||
// Right now in linux we hot replace the bin and request a restart
|
||||
// No assets are replaced
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bundle_update_linux(bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
use std::ffi::OsStr;
|
||||
|
||||
// build our app actually we support only appimage on linux
|
||||
if let Some(source_path) = bundles
|
||||
.iter()
|
||||
.filter(|bundle| bundle.package_type == crate::PackageType::AppImage)
|
||||
.find_map(|bundle| {
|
||||
bundle
|
||||
.bundle_paths
|
||||
.iter()
|
||||
.find(|path| path.extension() == Some(OsStr::new("AppImage")))
|
||||
})
|
||||
{
|
||||
// add .tar.gz to our path
|
||||
let appimage_archived = format!("{}.tar.gz", source_path.display());
|
||||
let appimage_archived_path = PathBuf::from(&appimage_archived);
|
||||
|
||||
// Create our gzip file
|
||||
create_tar(source_path, &appimage_archived_path)
|
||||
.with_context(|| "Failed to tar.gz update directory")?;
|
||||
|
||||
log::info!(action = "Bundling"; "{} ({})", appimage_archived, display_path(&appimage_archived_path));
|
||||
|
||||
Ok(vec![appimage_archived_path])
|
||||
} else {
|
||||
Err(crate::Error::UnableToFindProject)
|
||||
}
|
||||
}
|
||||
|
||||
// Create simple update-win_<arch>.zip
|
||||
// Including the binary as root
|
||||
// Right now in windows we hot replace the bin and request a restart
|
||||
// No assets are replaced
|
||||
fn bundle_update_windows(settings: &Settings, bundles: &[Bundle]) -> crate::Result<Vec<PathBuf>> {
|
||||
use crate::PackageType;
|
||||
use crate::bundle::settings::WebviewInstallMode;
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::bundle::windows::msi;
|
||||
use crate::bundle::windows::nsis;
|
||||
|
||||
// find our installers or rebuild
|
||||
let mut bundle_paths = Vec::new();
|
||||
let mut rebuild_installers = || -> crate::Result<()> {
|
||||
for bundle in bundles {
|
||||
match bundle.package_type {
|
||||
#[cfg(target_os = "windows")]
|
||||
PackageType::WindowsMsi => bundle_paths.extend(msi::bundle_project(settings, true)?),
|
||||
PackageType::Nsis => bundle_paths.extend(nsis::bundle_project(settings, true)?),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
|
||||
if matches!(
|
||||
settings.windows().webview_install_mode,
|
||||
WebviewInstallMode::OfflineInstaller { .. } | WebviewInstallMode::EmbedBootstrapper { .. }
|
||||
) {
|
||||
rebuild_installers()?;
|
||||
} else {
|
||||
let paths = bundles
|
||||
.iter()
|
||||
.filter(|bundle| {
|
||||
matches!(
|
||||
bundle.package_type,
|
||||
PackageType::WindowsMsi | PackageType::Nsis
|
||||
)
|
||||
})
|
||||
.flat_map(|bundle| bundle.bundle_paths.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// we expect our installer files to be on `bundle_paths`
|
||||
if paths.is_empty() {
|
||||
rebuild_installers()?;
|
||||
} else {
|
||||
bundle_paths.extend(paths);
|
||||
}
|
||||
};
|
||||
|
||||
let mut installers_archived_paths = Vec::new();
|
||||
for source_path in bundle_paths {
|
||||
// add .zip to our path
|
||||
let (archived_path, bundle_name) =
|
||||
source_path
|
||||
.components()
|
||||
.fold((PathBuf::new(), String::new()), |(mut p, mut b), c| {
|
||||
if let std::path::Component::Normal(name) = c {
|
||||
if let Some(name) = name.to_str() {
|
||||
// installers bundled for updater should be put in a directory named `${bundle_name}-updater`
|
||||
if name == WIX_UPDATER_OUTPUT_FOLDER_NAME || name == NSIS_UPDATER_OUTPUT_FOLDER_NAME {
|
||||
b = name.strip_suffix("-updater").unwrap().to_string();
|
||||
p.push(&b);
|
||||
return (p, b);
|
||||
}
|
||||
|
||||
if name == WIX_OUTPUT_FOLDER_NAME || name == NSIS_OUTPUT_FOLDER_NAME {
|
||||
b = name.to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
p.push(c);
|
||||
(p, b)
|
||||
});
|
||||
let archived_path = archived_path.with_extension(format!("{bundle_name}.zip"));
|
||||
|
||||
log::info!(action = "Bundling"; "{}", display_path(&archived_path));
|
||||
|
||||
// Create our gzip file
|
||||
create_zip(&source_path, &archived_path).with_context(|| "Failed to zip update bundle")?;
|
||||
|
||||
installers_archived_paths.push(archived_path);
|
||||
}
|
||||
|
||||
Ok(installers_archived_paths)
|
||||
}
|
||||
|
||||
pub fn create_zip(src_file: &Path, dst_file: &Path) -> crate::Result<PathBuf> {
|
||||
let parent_dir = dst_file.parent().expect("No data in parent");
|
||||
fs::create_dir_all(parent_dir)?;
|
||||
let writer = fs_utils::create_file(dst_file)?;
|
||||
|
||||
let file_name = src_file
|
||||
.file_name()
|
||||
.expect("Can't extract file name from path");
|
||||
|
||||
let mut zip = zip::ZipWriter::new(writer);
|
||||
let options = SimpleFileOptions::default()
|
||||
.compression_method(zip::CompressionMethod::Stored)
|
||||
.unix_permissions(0o755);
|
||||
|
||||
zip.start_file(file_name.to_string_lossy(), options)?;
|
||||
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)?;
|
||||
buffer.clear();
|
||||
|
||||
Ok(dst_file.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "macos"))]
|
||||
fn create_tar(src_dir: &Path, dest_path: &Path) -> crate::Result<PathBuf> {
|
||||
use flate2::{Compression, write::GzEncoder};
|
||||
|
||||
let dest_file = fs_utils::create_file(dest_path)?;
|
||||
let gzip_encoder = GzEncoder::new(dest_file, Compression::default());
|
||||
|
||||
let gzip_encoder = create_tar_from_src(src_dir, gzip_encoder)?;
|
||||
|
||||
let mut dest_file = gzip_encoder.finish()?;
|
||||
dest_file.flush()?;
|
||||
Ok(dest_path.to_owned())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn create_tar_from_src<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let mut builder = tar::Builder::new(dest_file);
|
||||
builder.follow_symlinks(false);
|
||||
builder.append_dir_all(src_dir.file_name().expect("Path has no file_name"), src_dir)?;
|
||||
builder.into_inner().map_err(Into::into)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn create_tar_from_src<P: AsRef<Path>, W: Write>(src_dir: P, dest_file: W) -> crate::Result<W> {
|
||||
let src_dir = src_dir.as_ref();
|
||||
let mut tar_builder = tar::Builder::new(dest_file);
|
||||
|
||||
// validate source type
|
||||
let file_type = fs::metadata(src_dir).expect("Can't read source directory");
|
||||
// if it's a file don't need to walkdir
|
||||
if file_type.is_file() {
|
||||
let mut src_file = fs::File::open(src_dir)?;
|
||||
let file_name = src_dir
|
||||
.file_name()
|
||||
.expect("Can't extract file name from path");
|
||||
|
||||
tar_builder.append_file(file_name, &mut src_file)?;
|
||||
} else {
|
||||
for entry in walkdir::WalkDir::new(src_dir) {
|
||||
let entry = entry?;
|
||||
let src_path = entry.path();
|
||||
if src_path == src_dir {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We add the .parent() because example if we send a path
|
||||
// /dev/src-tauri/target/debug/bundle/osx/app.app
|
||||
// We need a tar with app.app/<...> (source root folder should be included)
|
||||
// safe to unwrap: the path has a parent
|
||||
let dest_path = src_path.strip_prefix(src_dir.parent().unwrap())?;
|
||||
if entry.file_type().is_dir() {
|
||||
tar_builder.append_dir(dest_path, src_path)?;
|
||||
} else {
|
||||
let mut src_file = fs::File::open(src_path)?;
|
||||
tar_builder.append_file(dest_path, &mut src_file)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let dest_file = tar_builder.into_inner()?;
|
||||
Ok(dest_file)
|
||||
}
|
||||
16
crates/tauri-bundler/src/bundle/windows/mod.rs
Normal file
16
crates/tauri-bundler/src/bundle/windows/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod msi;
|
||||
|
||||
pub mod nsis;
|
||||
pub mod sign;
|
||||
|
||||
mod util;
|
||||
pub use util::{
|
||||
NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, WIX_OUTPUT_FOLDER_NAME,
|
||||
WIX_UPDATER_OUTPUT_FOLDER_NAME, vswhere_path,
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
<String Id="TauriLanguage">__language__</String>
|
||||
<String Id="TauriCodepage">__codepage__</String>
|
||||
<String Id="LaunchApp">Launch __productName__</String>
|
||||
<String Id="DowngradeErrorMessage">A newer version of __productName__ is already installed.</String>
|
||||
<String Id="PathEnvVarFeature">Add the install location of the __productName__ executable to the PATH system environment variable. This allows the __productName__ executable to be called from any location.</String>
|
||||
<String Id="InstallAppFeature">Installs __productName__.</String>
|
||||
28
crates/tauri-bundler/src/bundle/windows/msi/install-task.ps1
Normal file
28
crates/tauri-bundler/src/bundle/windows/msi/install-task.ps1
Normal file
@@ -0,0 +1,28 @@
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Adapted from https://superuser.com/a/532109
|
||||
param([string]$ChangeDir, [switch]$Elevated)
|
||||
|
||||
function Test-Admin {
|
||||
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
|
||||
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
|
||||
}
|
||||
|
||||
if ((Test-Admin) -eq $false) {
|
||||
if ($elevated) {
|
||||
# tried to elevate, did not work, aborting
|
||||
}
|
||||
else {
|
||||
$InstallDirectory = Get-Location
|
||||
$ArgList = ('-File "{0}" -ChangeDir "{1}" -Elevated' -f ($myinvocation.MyCommand.Definition, $InstallDirectory))
|
||||
Start-Process "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle hidden -Verb RunAs -ArgumentList $ArgList
|
||||
}
|
||||
exit
|
||||
}
|
||||
|
||||
if ($ChangeDir -ne "") {
|
||||
# Change directories to the install path
|
||||
Set-Location -Path $ChangeDir
|
||||
}
|
||||
SCHTASKS.EXE /CREATE /XML update.xml /TN "Update {{product_name}} - Skip UAC" /F
|
||||
154
crates/tauri-bundler/src/bundle/windows/msi/languages.json
Normal file
154
crates/tauri-bundler/src/bundle/windows/msi/languages.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"ar-SA": {
|
||||
"langId": 1025,
|
||||
"asciiCode": 1256
|
||||
},
|
||||
"ca-ES": {
|
||||
"langId": 1027,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"zh-TW": {
|
||||
"langId": 1028,
|
||||
"asciiCode": 950
|
||||
},
|
||||
"zh-CN": {
|
||||
"langId": 2052,
|
||||
"asciiCode": 936
|
||||
},
|
||||
"cs-CZ": {
|
||||
"langId": 1029,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"da-DK": {
|
||||
"langId": 1030,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"de-DE": {
|
||||
"langId": 1031,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"el-GR": {
|
||||
"langId": 1032,
|
||||
"asciiCode": 1253
|
||||
},
|
||||
"en-US": {
|
||||
"langId": 1033,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"es-ES": {
|
||||
"langId": 3082,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"et-EE": {
|
||||
"langId": 1061,
|
||||
"asciiCode": 1257
|
||||
},
|
||||
"fi-FI": {
|
||||
"langId": 1035,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"fr-FR": {
|
||||
"langId": 1036,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"he-IL": {
|
||||
"langId": 1037,
|
||||
"asciiCode": 1255
|
||||
},
|
||||
"hu-HU": {
|
||||
"langId": 1038,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"it-IT": {
|
||||
"langId": 1040,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"ja-JP": {
|
||||
"langId": 1041,
|
||||
"asciiCode": 932
|
||||
},
|
||||
"ko-KR": {
|
||||
"langId": 1042,
|
||||
"asciiCode": 949
|
||||
},
|
||||
"lt-LT": {
|
||||
"langId": 1063,
|
||||
"asciiCode": 1257
|
||||
},
|
||||
"lv-LV": {
|
||||
"langId": 1062,
|
||||
"asciiCode": 1257
|
||||
},
|
||||
"nl-NL": {
|
||||
"langId": 1043,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"nb-NO": {
|
||||
"langId": 1044,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"pl-PL": {
|
||||
"langId": 1045,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"pt-BR": {
|
||||
"langId": 1046,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"pt-PT": {
|
||||
"langId": 2070,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"ro-RO": {
|
||||
"langId": 1048,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"ru-RU": {
|
||||
"langId": 1049,
|
||||
"asciiCode": 1251
|
||||
},
|
||||
"hr-HR": {
|
||||
"langId": 1050,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"sk-SK": {
|
||||
"langId": 1051,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"sv-SE": {
|
||||
"langId": 1053,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"th-TH": {
|
||||
"langId": 1054,
|
||||
"asciiCode": 874
|
||||
},
|
||||
"tr-TR": {
|
||||
"langId": 1055,
|
||||
"asciiCode": 1254
|
||||
},
|
||||
"sl-SI": {
|
||||
"langId": 1060,
|
||||
"asciiCode": 1250
|
||||
},
|
||||
"vi-VN": {
|
||||
"langId": 1066,
|
||||
"asciiCode": 1258
|
||||
},
|
||||
"eu-ES": {
|
||||
"langId": 1069,
|
||||
"asciiCode": 1252
|
||||
},
|
||||
"bg-BG": {
|
||||
"langId": 1026,
|
||||
"asciiCode": 1251
|
||||
},
|
||||
"uk-UA": {
|
||||
"langId": 1058,
|
||||
"asciiCode": 1251
|
||||
},
|
||||
"sr-Latn-CS": {
|
||||
"langId": 2074,
|
||||
"asciiCode": 1250
|
||||
}
|
||||
}
|
||||
381
crates/tauri-bundler/src/bundle/windows/msi/main.wxs
Normal file
381
crates/tauri-bundler/src/bundle/windows/msi/main.wxs
Normal file
@@ -0,0 +1,381 @@
|
||||
<?if $(sys.BUILDARCH)="x86"?>
|
||||
<?define Win64 = "no" ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFilesFolder" ?>
|
||||
<?elseif $(sys.BUILDARCH)="x64"?>
|
||||
<?define Win64 = "yes" ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?elseif $(sys.BUILDARCH)="arm64"?>
|
||||
<?define Win64 = "yes" ?>
|
||||
<?define PlatformProgramFilesFolder = "ProgramFiles64Folder" ?>
|
||||
<?else?>
|
||||
<?error Unsupported value of sys.BUILDARCH=$(sys.BUILDARCH)?>
|
||||
<?endif?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product
|
||||
Id="*"
|
||||
Name="{{product_name}}"
|
||||
UpgradeCode="{{upgrade_code}}"
|
||||
Language="!(loc.TauriLanguage)"
|
||||
Manufacturer="{{manufacturer}}"
|
||||
Version="{{version}}">
|
||||
|
||||
<Package Id="*"
|
||||
Keywords="Installer"
|
||||
InstallerVersion="450"
|
||||
Languages="0"
|
||||
Compressed="yes"
|
||||
InstallScope="perMachine"
|
||||
SummaryCodepage="!(loc.TauriCodepage)"/>
|
||||
|
||||
<!-- https://docs.microsoft.com/en-us/windows/win32/msi/reinstallmode -->
|
||||
<!-- reinstall all files; rewrite all registry entries; reinstall all shortcuts -->
|
||||
<Property Id="REINSTALLMODE" Value="amus" />
|
||||
|
||||
<!-- Auto launch app after installation, useful for passive mode which usually used in updates -->
|
||||
<Property Id="AUTOLAUNCHAPP" Secure="yes" />
|
||||
<!-- Property to forward cli args to the launched app to not lose those of the pre-update instance -->
|
||||
<Property Id="LAUNCHAPPARGS" Secure="yes" />
|
||||
|
||||
{{#if allow_downgrades}}
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" AllowDowngrades="yes" />
|
||||
{{else}}
|
||||
<MajorUpgrade Schedule="afterInstallInitialize" DowngradeErrorMessage="!(loc.DowngradeErrorMessage)" AllowSameVersionUpgrades="yes" />
|
||||
{{/if}}
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<RemoveShortcuts>Installed AND NOT UPGRADINGPRODUCTCODE</RemoveShortcuts>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<Media Id="1" Cabinet="app.cab" EmbedCab="yes" />
|
||||
|
||||
{{#if banner_path}}
|
||||
<WixVariable Id="WixUIBannerBmp" Value="{{banner_path}}" />
|
||||
{{/if}}
|
||||
{{#if dialog_image_path}}
|
||||
<WixVariable Id="WixUIDialogBmp" Value="{{dialog_image_path}}" />
|
||||
{{/if}}
|
||||
{{#if license}}
|
||||
<WixVariable Id="WixUILicenseRtf" Value="{{license}}" />
|
||||
{{/if}}
|
||||
|
||||
<Icon Id="ProductIcon" SourceFile="{{icon_path}}"/>
|
||||
<Property Id="ARPPRODUCTICON" Value="ProductIcon" />
|
||||
<Property Id="ARPNOREPAIR" Value="yes" Secure="yes" /> <!-- Remove repair -->
|
||||
<SetProperty Id="ARPNOMODIFY" Value="1" After="InstallValidate" Sequence="execute"/>
|
||||
|
||||
{{#if homepage}}
|
||||
<Property Id="ARPURLINFOABOUT" Value="{{homepage}}"/>
|
||||
<Property Id="ARPHELPLINK" Value="{{homepage}}"/>
|
||||
<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 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 -->
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="!(loc.LaunchApp)" />
|
||||
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOX" Value="1"/>
|
||||
<CustomAction Id="LaunchApplication" Impersonate="yes" FileKey="Path" ExeCommand="[LAUNCHAPPARGS]" Return="asyncNoWait" />
|
||||
|
||||
<UI>
|
||||
<!-- launch app checkbox -->
|
||||
<Publish Dialog="ExitDialog" Control="Finish" Event="DoAction" Value="LaunchApplication">WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed</Publish>
|
||||
|
||||
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLDIR" />
|
||||
|
||||
{{#unless license}}
|
||||
<!-- Skip license dialog -->
|
||||
<Publish Dialog="WelcomeDlg"
|
||||
Control="Next"
|
||||
Event="NewDialog"
|
||||
Value="InstallDirDlg"
|
||||
Order="2">1</Publish>
|
||||
<Publish Dialog="InstallDirDlg"
|
||||
Control="Back"
|
||||
Event="NewDialog"
|
||||
Value="WelcomeDlg"
|
||||
Order="2">1</Publish>
|
||||
{{/unless}}
|
||||
</UI>
|
||||
|
||||
<UIRef Id="WixUI_InstallDir" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="DesktopFolder" Name="Desktop">
|
||||
<Component Id="ApplicationShortcutDesktop" Guid="*">
|
||||
<Shortcut Id="ApplicationDesktopShortcut" Name="{{product_name}}" Description="Runs {{product_name}}" Target="[!Path]" WorkingDirectory="INSTALLDIR" />
|
||||
<RemoveFolder Id="DesktopFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Desktop Shortcut" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
<Directory Id="$(var.PlatformProgramFilesFolder)" Name="PFiles">
|
||||
<Directory Id="INSTALLDIR" Name="{{product_name}}"/>
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="{{product_name}}"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLDIR">
|
||||
<Component Id="RegistryEntries" Guid="*">
|
||||
<RegistryKey Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}">
|
||||
<RegistryValue Name="InstallDir" Type="string" Value="[INSTALLDIR]" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<!-- Change the Root to HKCU for perUser installations -->
|
||||
{{#each deep_link_protocols as |protocol| ~}}
|
||||
<RegistryKey Root="HKLM" Key="Software\Classes\\{{protocol}}">
|
||||
<RegistryValue Type="string" Name="URL Protocol" Value=""/>
|
||||
<RegistryValue Type="string" Value="URL:{{bundle_id}} protocol"/>
|
||||
<RegistryKey Key="DefaultIcon">
|
||||
<RegistryValue Type="string" Value=""[!Path]",0" />
|
||||
</RegistryKey>
|
||||
<RegistryKey Key="shell\open\command">
|
||||
<RegistryValue Type="string" Value=""[!Path]" "%1"" />
|
||||
</RegistryKey>
|
||||
</RegistryKey>
|
||||
{{/each~}}
|
||||
</Component>
|
||||
<Component Id="Path" Guid="{{path_component_guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Path" Source="{{main_binary_path}}" KeyPath="yes" Checksum="yes"/>
|
||||
{{#each file_associations as |association| ~}}
|
||||
{{#each association.ext as |ext| ~}}
|
||||
<ProgId Id="{{../../product_name}}.{{ext}}" Advertise="yes" Description="{{association.description}}">
|
||||
<Extension Id="{{ext}}" Advertise="yes">
|
||||
<Verb Id="open" Command="Open with {{../../product_name}}" Argument=""%1"" />
|
||||
</Extension>
|
||||
</ProgId>
|
||||
{{/each~}}
|
||||
{{/each~}}
|
||||
</Component>
|
||||
{{#each binaries as |bin| ~}}
|
||||
<Component Id="{{ bin.id }}" Guid="{{bin.guid}}" Win64="$(var.Win64)">
|
||||
<File Id="Bin_{{ bin.id }}" Source="{{bin.path}}" KeyPath="yes"/>
|
||||
</Component>
|
||||
{{/each~}}
|
||||
{{#if enable_elevated_update_task}}
|
||||
<Component Id="UpdateTask" Guid="C492327D-9720-4CD5-8DB8-F09082AF44BE" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTask" Source="update.xml" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
<Component Id="UpdateTaskInstaller" Guid="011F25ED-9BE3-50A7-9E9B-3519ED2B9932" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTaskInstaller" Source="install-task.ps1" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
<Component Id="UpdateTaskUninstaller" Guid="D4F6CC3F-32DC-5FD0-95E8-782FFD7BBCE1" Win64="$(var.Win64)">
|
||||
<File Id="UpdateTaskUninstaller" Source="uninstall-task.ps1" KeyPath="yes" Checksum="yes"/>
|
||||
</Component>
|
||||
{{/if}}
|
||||
{{resources}}
|
||||
<Component Id="CMP_UninstallShortcut" Guid="*">
|
||||
|
||||
<Shortcut Id="UninstallShortcut"
|
||||
Name="Uninstall {{product_name}}"
|
||||
Description="Uninstalls {{product_name}}"
|
||||
Target="[System64Folder]msiexec.exe"
|
||||
Arguments="/x [ProductCode]" />
|
||||
|
||||
<RemoveFolder Id="INSTALLDIR"
|
||||
On="uninstall" />
|
||||
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\\{{manufacturer}}\\{{product_name}}"
|
||||
Name="Uninstaller Shortcut"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="ApplicationShortcut" Guid="*">
|
||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||
Name="{{product_name}}"
|
||||
Description="Runs {{product_name}}"
|
||||
Target="[!Path]"
|
||||
Icon="ProductIcon"
|
||||
WorkingDirectory="INSTALLDIR">
|
||||
<ShortcutProperty Key="System.AppUserModel.ID" Value="{{bundle_id}}"/>
|
||||
</Shortcut>
|
||||
<RemoveFolder Id="ApplicationProgramsFolder" On="uninstall"/>
|
||||
<RegistryValue Root="HKCU" Key="Software\\{{manufacturer}}\\{{product_name}}" Name="Start Menu Shortcut" Type="integer" Value="1" KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
{{#each merge_modules as |msm| ~}}
|
||||
<DirectoryRef Id="TARGETDIR">
|
||||
<Merge Id="{{ msm.name }}" SourceFile="{{ msm.path }}" DiskId="1" Language="!(loc.TauriLanguage)" />
|
||||
</DirectoryRef>
|
||||
|
||||
<Feature Id="{{ msm.name }}" Title="{{ msm.name }}" AllowAdvertise="no" Display="hidden" Level="1">
|
||||
<MergeRef Id="{{ msm.name }}"/>
|
||||
</Feature>
|
||||
{{/each~}}
|
||||
|
||||
<Feature
|
||||
Id="MainProgram"
|
||||
Title="Application"
|
||||
Description="!(loc.InstallAppFeature)"
|
||||
Level="1"
|
||||
ConfigurableDirectory="INSTALLDIR"
|
||||
AllowAdvertise="no"
|
||||
Display="expand"
|
||||
Absent="disallow">
|
||||
|
||||
<ComponentRef Id="RegistryEntries"/>
|
||||
|
||||
{{#each resource_file_ids as |resource_file_id| ~}}
|
||||
<ComponentRef Id="{{ resource_file_id }}"/>
|
||||
{{/each~}}
|
||||
|
||||
{{#if enable_elevated_update_task}}
|
||||
<ComponentRef Id="UpdateTask" />
|
||||
<ComponentRef Id="UpdateTaskInstaller" />
|
||||
<ComponentRef Id="UpdateTaskUninstaller" />
|
||||
{{/if}}
|
||||
|
||||
<Feature Id="ShortcutsFeature"
|
||||
Title="Shortcuts"
|
||||
Level="1">
|
||||
<ComponentRef Id="Path"/>
|
||||
<ComponentRef Id="CMP_UninstallShortcut" />
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
<ComponentRef Id="ApplicationShortcutDesktop" />
|
||||
</Feature>
|
||||
|
||||
<Feature
|
||||
Id="Environment"
|
||||
Title="PATH Environment Variable"
|
||||
Description="!(loc.PathEnvVarFeature)"
|
||||
Level="1"
|
||||
Absent="allow">
|
||||
<ComponentRef Id="Path"/>
|
||||
{{#each binaries as |bin| ~}}
|
||||
<ComponentRef Id="{{ bin.id }}"/>
|
||||
{{/each~}}
|
||||
</Feature>
|
||||
</Feature>
|
||||
|
||||
<Feature Id="External" AllowAdvertise="no" Absent="disallow">
|
||||
{{#each component_group_refs as |id| ~}}
|
||||
<ComponentGroupRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each component_refs as |id| ~}}
|
||||
<ComponentRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each feature_group_refs as |id| ~}}
|
||||
<FeatureGroupRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each feature_refs as |id| ~}}
|
||||
<FeatureRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
{{#each merge_refs as |id| ~}}
|
||||
<MergeRef Id="{{ id }}"/>
|
||||
{{/each~}}
|
||||
</Feature>
|
||||
|
||||
{{#if install_webview}}
|
||||
<!-- WebView2 -->
|
||||
<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}} '/install') -Wait' Return='check'/>
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='DownloadAndInvokeBootstrapper' Before='InstallFinalize'>
|
||||
<![CDATA[NOT(REMOVE OR INSTALLED_WEBVIEW2_VERSION)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
{{#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 INSTALLED_WEBVIEW2_VERSION)]]>
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
{{#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 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}&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}}
|
||||
|
||||
{{/if}}
|
||||
|
||||
{{#if enable_elevated_update_task}}
|
||||
<!-- Install an elevated update task within Windows Task Scheduler -->
|
||||
<CustomAction
|
||||
Id="CreateUpdateTask"
|
||||
Return="check"
|
||||
Directory="INSTALLDIR"
|
||||
Execute="commit"
|
||||
Impersonate="yes"
|
||||
ExeCommand="powershell.exe -WindowStyle hidden .\install-task.ps1" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action='CreateUpdateTask' Before='InstallFinalize'>
|
||||
NOT(REMOVE)
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
<!-- Remove elevated update task during uninstall -->
|
||||
<CustomAction
|
||||
Id="DeleteUpdateTask"
|
||||
Return="check"
|
||||
Directory="INSTALLDIR"
|
||||
ExeCommand="powershell.exe -WindowStyle hidden .\uninstall-task.ps1" />
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="DeleteUpdateTask" Before='InstallFinalize'>
|
||||
(REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE
|
||||
</Custom>
|
||||
</InstallExecuteSequence>
|
||||
{{/if}}
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="LaunchApplication" After="InstallFinalize">AUTOLAUNCHAPP AND NOT Installed</Custom>
|
||||
</InstallExecuteSequence>
|
||||
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLDIR]" After="CostFinalize"/>
|
||||
</Product>
|
||||
</Wix>
|
||||
1154
crates/tauri-bundler/src/bundle/windows/msi/mod.rs
Normal file
1154
crates/tauri-bundler/src/bundle/windows/msi/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
# Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-License-Identifier: MIT
|
||||
# Adapted from https://superuser.com/a/532109
|
||||
param([switch]$Elevated)
|
||||
|
||||
function Test-Admin {
|
||||
$currentUser = New-Object Security.Principal.WindowsPrincipal $([Security.Principal.WindowsIdentity]::GetCurrent())
|
||||
$currentUser.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
|
||||
}
|
||||
|
||||
if ((Test-Admin) -eq $false) {
|
||||
if ($elevated) {
|
||||
# tried to elevate, did not work, aborting
|
||||
}
|
||||
else {
|
||||
$ArgList = ('-File "{0}" -Elevated' -f $myinvocation.MyCommand.Definition)
|
||||
Start-Process "$env:SYSTEMROOT\System32\WindowsPowerShell\v1.0\powershell.exe" -WindowStyle hidden -Verb RunAs -ArgumentList $ArgList
|
||||
}
|
||||
exit
|
||||
}
|
||||
|
||||
SCHTASKS.EXE /DELETE /TN 'Update {{product_name}} - Skip UAC' /F
|
||||
43
crates/tauri-bundler/src/bundle/windows/msi/update-task.xml
Normal file
43
crates/tauri-bundler/src/bundle/windows/msi/update-task.xml
Normal file
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<!--
|
||||
Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<URI>\Update {{product_name}} - Skip UAC</URI>
|
||||
</RegistrationInfo>
|
||||
<Triggers />
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>Parallel</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>false</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>true</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>cmd.exe</Command>
|
||||
<Arguments>/c ^"%SYSTEMROOT%\System32\msiexec.exe /i "%TEMP%\\{{product_name}}.msi" {{msiexec_args}} /promptrestart^"</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
135
crates/tauri-bundler/src/bundle/windows/nsis/FileAssociation.nsh
Normal file
135
crates/tauri-bundler/src/bundle/windows/nsis/FileAssociation.nsh
Normal file
@@ -0,0 +1,135 @@
|
||||
; from https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
; fileassoc.nsh
|
||||
; File association helper macros
|
||||
; Written by Saivert
|
||||
;
|
||||
; Improved by Nikku<https://github.com/nikku>.
|
||||
;
|
||||
; Features automatic backup system and UPDATEFILEASSOC macro for
|
||||
; shell change notification.
|
||||
;
|
||||
; |> How to use <|
|
||||
; To associate a file with an application so you can double-click it in explorer, use
|
||||
; the APP_ASSOCIATE macro like this:
|
||||
;
|
||||
; Example:
|
||||
; !insertmacro APP_ASSOCIATE "txt" "myapp.textfile" "Description of txt files" \
|
||||
; "$INSTDIR\myapp.exe,0" "Open with myapp" "$INSTDIR\myapp.exe $\"%1$\""
|
||||
;
|
||||
; Never insert the APP_ASSOCIATE macro multiple times, it is only meant
|
||||
; to associate an application with a single file and using the
|
||||
; the "open" verb as default. To add more verbs (actions) to a file
|
||||
; use the APP_ASSOCIATE_ADDVERB macro.
|
||||
;
|
||||
; Example:
|
||||
; !insertmacro APP_ASSOCIATE_ADDVERB "myapp.textfile" "edit" "Edit with myapp" \
|
||||
; "$INSTDIR\myapp.exe /edit $\"%1$\""
|
||||
;
|
||||
; To have access to more options when registering the file association use the
|
||||
; APP_ASSOCIATE_EX macro. Here you can specify the verb and what verb is to be the
|
||||
; standard action (default verb).
|
||||
;
|
||||
; Note, that this script takes into account user versus global installs.
|
||||
; To properly work you must initialize the SHELL_CONTEXT variable via SetShellVarContext.
|
||||
;
|
||||
; And finally: To remove the association from the registry use the APP_UNASSOCIATE
|
||||
; macro. Here is another example just to wrap it up:
|
||||
; !insertmacro APP_UNASSOCIATE "txt" "myapp.textfile"
|
||||
;
|
||||
; |> Note <|
|
||||
; When defining your file class string always use the short form of your application title
|
||||
; then a period (dot) and the type of file. This keeps the file class sort of unique.
|
||||
; Examples:
|
||||
; Winamp.Playlist
|
||||
; NSIS.Script
|
||||
; Photoshop.JPEGFile
|
||||
;
|
||||
; |> Tech info <|
|
||||
; The registry key layout for a global file association is:
|
||||
;
|
||||
; HKEY_LOCAL_MACHINE\Software\Classes
|
||||
; <".ext"> = <applicationID>
|
||||
; <applicationID> = <"description">
|
||||
; shell
|
||||
; <verb> = <"menu-item text">
|
||||
; command = <"command string">
|
||||
;
|
||||
;
|
||||
; The registry key layout for a per-user file association is:
|
||||
;
|
||||
; HKEY_CURRENT_USER\Software\Classes
|
||||
; <".ext"> = <applicationID>
|
||||
; <applicationID> = <"description">
|
||||
; shell
|
||||
; <verb> = <"menu-item text">
|
||||
; command = <"command string">
|
||||
;
|
||||
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_ASSOCIATE_EX EXT FILECLASS DESCRIPTION ICON VERB DEFAULTVERB SHELLNEW COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
StrCmp "${SHELLNEW}" "0" +2
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}\ShellNew" "NullFile" ""
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" `${DEFAULTVERB}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_ASSOCIATE_ADDVERB FILECLASS VERB COMMANDTEXT COMMAND
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\${VERB}\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_ASSOCIATE_REMOVEVERB FILECLASS VERB
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}\shell\${VERB}`
|
||||
!macroend
|
||||
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro APP_ASSOCIATE_GETFILECLASS OUTPUT EXT
|
||||
ReadRegStr ${OUTPUT} SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
!macroend
|
||||
|
||||
|
||||
; !defines for use with SHChangeNotify
|
||||
!ifdef SHCNE_ASSOCCHANGED
|
||||
!undef SHCNE_ASSOCCHANGED
|
||||
!endif
|
||||
!define SHCNE_ASSOCCHANGED 0x08000000
|
||||
!ifdef SHCNF_FLUSH
|
||||
!undef SHCNF_FLUSH
|
||||
!endif
|
||||
!define SHCNF_FLUSH 0x1000
|
||||
|
||||
!macro UPDATEFILEASSOC
|
||||
; Using the system.dll plugin to call the SHChangeNotify Win32 API function so we
|
||||
; can update the shell.
|
||||
System::Call "shell32::SHChangeNotify(i,i,i,i) (${SHCNE_ASSOCCHANGED}, ${SHCNF_FLUSH}, 0, 0)"
|
||||
!macroend
|
||||
978
crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi
Normal file
978
crates/tauri-bundler/src/bundle/windows/nsis/installer.nsi
Normal file
@@ -0,0 +1,978 @@
|
||||
Unicode true
|
||||
ManifestDPIAware true
|
||||
; Add in `dpiAwareness` `PerMonitorV2` to manifest for Windows 10 1607+ (note this should not affect lower versions since they should be able to ignore this and pick up `dpiAware` `true` set by `ManifestDPIAware true`)
|
||||
; Currently undocumented on NSIS's website but is in the Docs folder of source tree, see
|
||||
; https://github.com/kichik/nsis/blob/5fc0b87b819a9eec006df4967d08e522ddd651c9/Docs/src/attributes.but#L286-L300
|
||||
; https://github.com/tauri-apps/tauri/pull/10106
|
||||
ManifestDPIAwareness PerMonitorV2
|
||||
|
||||
!if "{{compression}}" == "none"
|
||||
SetCompress off
|
||||
!else
|
||||
; Set the compression algorithm. We default to LZMA.
|
||||
SetCompressor /SOLID "{{compression}}"
|
||||
!endif
|
||||
|
||||
; Keep above !include to stay ahead of any plugin command
|
||||
; see https://github.com/tauri-apps/tauri/pull/15422#discussion_r3289239624
|
||||
{{#if signed_plugins_path}}
|
||||
!addplugindir "{{signed_plugins_path}}"
|
||||
{{/if}}
|
||||
|
||||
!include MUI2.nsh
|
||||
!include FileFunc.nsh
|
||||
!include x64.nsh
|
||||
!include WordFunc.nsh
|
||||
!include "utils.nsh"
|
||||
!include "FileAssociation.nsh"
|
||||
!include "Win\COM.nsh"
|
||||
!include "Win\Propkey.nsh"
|
||||
!include "Win\RestartManager.nsh"
|
||||
!include "StrFunc.nsh"
|
||||
${StrCase}
|
||||
${StrLoc}
|
||||
|
||||
{{#if installer_hooks}}
|
||||
!include "{{installer_hooks}}"
|
||||
{{/if}}
|
||||
|
||||
!define WEBVIEW2APPGUID "{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
|
||||
|
||||
!define MANUFACTURER "{{manufacturer}}"
|
||||
!define PRODUCTNAME "{{product_name}}"
|
||||
!define VERSION "{{version}}"
|
||||
!define VERSIONWITHBUILD "{{version_with_build}}"
|
||||
!define HOMEPAGE "{{homepage}}"
|
||||
!define INSTALLMODE "{{install_mode}}"
|
||||
!define LICENSE "{{license}}"
|
||||
!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}}"
|
||||
!define COPYRIGHT "{{copyright}}"
|
||||
!define OUTFILE "{{out_file}}"
|
||||
!define ARCH "{{arch}}"
|
||||
!define ADDITIONALPLUGINSPATH "{{additional_plugins_path}}"
|
||||
!define ALLOWDOWNGRADES "{{allow_downgrades}}"
|
||||
!define DISPLAYLANGUAGESELECTOR "{{display_language_selector}}"
|
||||
!define INSTALLWEBVIEW2MODE "{{install_webview2_mode}}"
|
||||
!define WEBVIEW2INSTALLERARGS "{{webview2_installer_args}}"
|
||||
!define WEBVIEW2BOOTSTRAPPERPATH "{{webview2_bootstrapper_path}}"
|
||||
!define WEBVIEW2INSTALLERPATH "{{webview2_installer_path}}"
|
||||
!define MINIMUMWEBVIEW2VERSION "{{minimum_webview2_version}}"
|
||||
!define UNINSTKEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCTNAME}"
|
||||
!define MANUKEY "Software\${MANUFACTURER}"
|
||||
!define MANUPRODUCTKEY "${MANUKEY}\${PRODUCTNAME}"
|
||||
!define UNINSTALLERSIGNCOMMAND "{{uninstaller_sign_cmd}}"
|
||||
!define ESTIMATEDSIZE "{{estimated_size}}"
|
||||
!define STARTMENUFOLDER "{{start_menu_folder}}"
|
||||
|
||||
Var PassiveMode
|
||||
Var UpdateMode
|
||||
Var NoShortcutMode
|
||||
Var WixMode
|
||||
Var OldMainBinaryName
|
||||
|
||||
Name "${PRODUCTNAME}"
|
||||
BrandingText "${COPYRIGHT}"
|
||||
OutFile "${OUTFILE}"
|
||||
|
||||
; We don't actually use this value as default install path,
|
||||
; it's just for nsis to append the product name folder in the directory selector
|
||||
; https://nsis.sourceforge.io/Reference/InstallDir
|
||||
!define PLACEHOLDER_INSTALL_DIR "placeholder\${PRODUCTNAME}"
|
||||
InstallDir "${PLACEHOLDER_INSTALL_DIR}"
|
||||
|
||||
VIProductVersion "${VERSIONWITHBUILD}"
|
||||
VIAddVersionKey "ProductName" "${PRODUCTNAME}"
|
||||
VIAddVersionKey "FileDescription" "${PRODUCTNAME}"
|
||||
VIAddVersionKey "LegalCopyright" "${COPYRIGHT}"
|
||||
VIAddVersionKey "FileVersion" "${VERSION}"
|
||||
VIAddVersionKey "ProductVersion" "${VERSION}"
|
||||
|
||||
# additional plugins
|
||||
!addplugindir "${ADDITIONALPLUGINSPATH}"
|
||||
|
||||
; Uninstaller signing command
|
||||
!if "${UNINSTALLERSIGNCOMMAND}" != ""
|
||||
!uninstfinalize '${UNINSTALLERSIGNCOMMAND}'
|
||||
!endif
|
||||
|
||||
; Handle install mode, `perUser`, `perMachine` or `both`
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
RequestExecutionLevel admin
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
RequestExecutionLevel user
|
||||
!endif
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!define MULTIUSER_MUI
|
||||
!define MULTIUSER_INSTALLMODE_INSTDIR "${PRODUCTNAME}"
|
||||
!define MULTIUSER_INSTALLMODE_COMMANDLINE
|
||||
!if "${ARCH}" == "x64"
|
||||
!define MULTIUSER_USE_PROGRAMFILES64
|
||||
!else if "${ARCH}" == "arm64"
|
||||
!define MULTIUSER_USE_PROGRAMFILES64
|
||||
!endif
|
||||
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_KEY "${UNINSTKEY}"
|
||||
!define MULTIUSER_INSTALLMODE_DEFAULT_REGISTRY_VALUENAME "CurrentUser"
|
||||
!define MULTIUSER_INSTALLMODEPAGE_SHOWUSERNAME
|
||||
!define MULTIUSER_INSTALLMODE_FUNCTION RestorePreviousInstallLocation
|
||||
!define MULTIUSER_EXECUTIONLEVEL Highest
|
||||
!include MultiUser.nsh
|
||||
!endif
|
||||
|
||||
; Installer icon
|
||||
!if "${INSTALLERICON}" != ""
|
||||
!define MUI_ICON "${INSTALLERICON}"
|
||||
!endif
|
||||
|
||||
; Installer sidebar image
|
||||
!if "${SIDEBARIMAGE}" != ""
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "${SIDEBARIMAGE}"
|
||||
!endif
|
||||
|
||||
; Enable header images for installer and uninstaller pages when either image is configured.
|
||||
!if "${HEADERIMAGE}" != ""
|
||||
!define MUI_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
|
||||
!define MUI_LANGDLL_REGISTRY_ROOT "HKCU"
|
||||
!define MUI_LANGDLL_REGISTRY_KEY "${MANUPRODUCTKEY}"
|
||||
!define MUI_LANGDLL_REGISTRY_VALUENAME "Installer Language"
|
||||
|
||||
; Installer pages, must be ordered as they appear
|
||||
; 1. Welcome Page
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
|
||||
; 2. License Page (if defined)
|
||||
!if "${LICENSE}" != ""
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_LICENSE "${LICENSE}"
|
||||
!endif
|
||||
|
||||
; 3. Install mode (if it is set to `both`)
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MULTIUSER_PAGE_INSTALLMODE
|
||||
!endif
|
||||
|
||||
; 4. Custom page to ask user if he wants to reinstall/uninstall
|
||||
; only if a previous installation was detected
|
||||
Var ReinstallPageCheck
|
||||
Page custom PageReinstall PageLeaveReinstall
|
||||
Function PageReinstall
|
||||
; Uninstall previous WiX installation if exists.
|
||||
;
|
||||
; A WiX installer stores the installation info in registry
|
||||
; using a UUID and so we have to loop through all keys under
|
||||
; `HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`
|
||||
; and check if `DisplayName` and `Publisher` keys match ${PRODUCTNAME} and ${MANUFACTURER}
|
||||
;
|
||||
; This has a potential issue that there maybe another installation that matches
|
||||
; our ${PRODUCTNAME} and ${MANUFACTURER} but wasn't installed by our WiX installer,
|
||||
; however, this should be fine since the user will have to confirm the uninstallation
|
||||
; and they can chose to abort it if doesn't make sense.
|
||||
StrCpy $0 0
|
||||
wix_loop:
|
||||
EnumRegKey $1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" $0
|
||||
StrCmp $1 "" wix_loop_done ; Exit loop if there is no more keys to loop on
|
||||
IntOp $0 $0 + 1
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "DisplayName"
|
||||
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "Publisher"
|
||||
StrCmp "$R0$R1" "${PRODUCTNAME}${MANUFACTURER}" 0 wix_loop
|
||||
ReadRegStr $R0 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1" "UninstallString"
|
||||
${StrCase} $R1 $R0 "L"
|
||||
${StrLoc} $R0 $R1 "msiexec" ">"
|
||||
StrCmp $R0 0 0 wix_loop_done
|
||||
StrCpy $WixMode 1
|
||||
StrCpy $R6 "SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$1"
|
||||
Goto compare_version
|
||||
wix_loop_done:
|
||||
|
||||
; Check if there is an existing installation, if not, abort the reinstall page
|
||||
ReadRegStr $R0 SHCTX "${UNINSTKEY}" ""
|
||||
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||
${IfThen} "$R0$R1" == "" ${|} Abort ${|}
|
||||
|
||||
; Compare this installar version with the existing installation
|
||||
; and modify the messages presented to the user accordingly
|
||||
compare_version:
|
||||
StrCpy $R4 "$(older)"
|
||||
${If} $WixMode = 1
|
||||
ReadRegStr $R0 HKLM "$R6" "DisplayVersion"
|
||||
${Else}
|
||||
ReadRegStr $R0 SHCTX "${UNINSTKEY}" "DisplayVersion"
|
||||
${EndIf}
|
||||
${IfThen} $R0 == "" ${|} StrCpy $R4 "$(unknown)" ${|}
|
||||
|
||||
nsis_tauri_utils::SemverCompare "${VERSION}" $R0
|
||||
Pop $R0
|
||||
; Reinstalling the same version
|
||||
${If} $R0 = 0
|
||||
StrCpy $R1 "$(alreadyInstalledLong)"
|
||||
StrCpy $R2 "$(addOrReinstall)"
|
||||
StrCpy $R3 "$(uninstallApp)"
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(chooseMaintenanceOption)"
|
||||
; Upgrading
|
||||
${ElseIf} $R0 = 1
|
||||
StrCpy $R1 "$(olderOrUnknownVersionInstalled)"
|
||||
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||
StrCpy $R3 "$(dontUninstall)"
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||
; Downgrading
|
||||
${ElseIf} $R0 = -1
|
||||
StrCpy $R1 "$(newerVersionInstalled)"
|
||||
StrCpy $R2 "$(uninstallBeforeInstalling)"
|
||||
!if "${ALLOWDOWNGRADES}" == "true"
|
||||
StrCpy $R3 "$(dontUninstall)"
|
||||
!else
|
||||
StrCpy $R3 "$(dontUninstallDowngrade)"
|
||||
!endif
|
||||
!insertmacro MUI_HEADER_TEXT "$(alreadyInstalled)" "$(choowHowToInstall)"
|
||||
${Else}
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; Skip showing the page if passive
|
||||
;
|
||||
; Note that we don't call this earlier at the beginning
|
||||
; of this function because we need to populate some variables
|
||||
; related to current installed version if detected and whether
|
||||
; we are downgrading or not.
|
||||
${If} $PassiveMode = 1
|
||||
Call PageLeaveReinstall
|
||||
${Else}
|
||||
nsDialogs::Create 1018
|
||||
Pop $R4
|
||||
${IfThen} $(^RTL) = 1 ${|} nsDialogs::SetRTL $(^RTL) ${|}
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 24u $R1
|
||||
Pop $R1
|
||||
|
||||
${NSD_CreateRadioButton} 30u 50u -30u 8u $R2
|
||||
Pop $R2
|
||||
${NSD_OnClick} $R2 PageReinstallUpdateSelection
|
||||
|
||||
${NSD_CreateRadioButton} 30u 70u -30u 8u $R3
|
||||
Pop $R3
|
||||
; Disable this radio button if downgrading and downgrades are disabled
|
||||
!if "${ALLOWDOWNGRADES}" == "false"
|
||||
${IfThen} $R0 = -1 ${|} EnableWindow $R3 0 ${|}
|
||||
!endif
|
||||
${NSD_OnClick} $R3 PageReinstallUpdateSelection
|
||||
|
||||
; Check the first radio button if this the first time
|
||||
; we enter this page or if the second button wasn't
|
||||
; selected the last time we were on this page
|
||||
${If} $ReinstallPageCheck <> 2
|
||||
SendMessage $R2 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||
${Else}
|
||||
SendMessage $R3 ${BM_SETCHECK} ${BST_CHECKED} 0
|
||||
${EndIf}
|
||||
|
||||
${NSD_SetFocus} $R2
|
||||
nsDialogs::Show
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
Function PageReinstallUpdateSelection
|
||||
${NSD_GetState} $R2 $R1
|
||||
${If} $R1 == ${BST_CHECKED}
|
||||
StrCpy $ReinstallPageCheck 1
|
||||
${Else}
|
||||
StrCpy $ReinstallPageCheck 2
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
Function PageLeaveReinstall
|
||||
${NSD_GetState} $R2 $R1
|
||||
|
||||
; If migrating from Wix, always uninstall
|
||||
${If} $WixMode = 1
|
||||
Goto reinst_uninstall
|
||||
${EndIf}
|
||||
|
||||
; In update mode, always proceeds without uninstalling
|
||||
${If} $UpdateMode = 1
|
||||
Goto reinst_done
|
||||
${EndIf}
|
||||
|
||||
; $R0 holds whether same(0)/upgrading(1)/downgrading(-1) version
|
||||
; $R1 holds the radio buttons state:
|
||||
; 1 => first choice was selected
|
||||
; 0 => second choice was selected
|
||||
${If} $R0 = 0 ; Same version, proceed
|
||||
${If} $R1 = 1 ; User chose to add/reinstall
|
||||
Goto reinst_done
|
||||
${Else} ; User chose to uninstall
|
||||
Goto reinst_uninstall
|
||||
${EndIf}
|
||||
${ElseIf} $R0 = 1 ; Upgrading
|
||||
${If} $R1 = 1 ; User chose to uninstall
|
||||
Goto reinst_uninstall
|
||||
${Else}
|
||||
Goto reinst_done ; User chose NOT to uninstall
|
||||
${EndIf}
|
||||
${ElseIf} $R0 = -1 ; Downgrading
|
||||
${If} $R1 = 1 ; User chose to uninstall
|
||||
Goto reinst_uninstall
|
||||
${Else}
|
||||
Goto reinst_done ; User chose NOT to uninstall
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
reinst_uninstall:
|
||||
HideWindow
|
||||
ClearErrors
|
||||
|
||||
${If} $WixMode = 1
|
||||
ReadRegStr $R1 HKLM "$R6" "UninstallString"
|
||||
ExecWait '$R1' $0
|
||||
${Else}
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
ReadRegStr $R1 SHCTX "${UNINSTKEY}" "UninstallString"
|
||||
${IfThen} $UpdateMode = 1 ${|} StrCpy $R1 "$R1 /UPDATE" ${|} ; append /UPDATE
|
||||
${IfThen} $PassiveMode = 1 ${|} StrCpy $R1 "$R1 /P" ${|} ; append /P
|
||||
StrCpy $R1 "$R1 _?=$4" ; append uninstall directory
|
||||
ExecWait '$R1' $0
|
||||
${EndIf}
|
||||
|
||||
BringToFront
|
||||
|
||||
${IfThen} ${Errors} ${|} StrCpy $0 2 ${|} ; ExecWait failed, set fake exit code
|
||||
|
||||
${If} $0 <> 0
|
||||
${OrIf} ${FileExists} "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
; User cancelled wix uninstaller? return to select un/reinstall page
|
||||
${If} $WixMode = 1
|
||||
${AndIf} $0 = 1602
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; User cancelled NSIS uninstaller? return to select un/reinstall page
|
||||
${If} $0 = 1
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; Other errors? show generic error message and return to select un/reinstall page
|
||||
MessageBox MB_ICONEXCLAMATION "$(unableToUninstall)"
|
||||
Abort
|
||||
${EndIf}
|
||||
reinst_done:
|
||||
FunctionEnd
|
||||
|
||||
; 5. Choose install directory page
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
|
||||
; 6. Start menu shortcut page
|
||||
Var AppStartMenuFolder
|
||||
!if "${STARTMENUFOLDER}" != ""
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!define MUI_STARTMENUPAGE_DEFAULTFOLDER "${STARTMENUFOLDER}"
|
||||
!else
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE Skip
|
||||
!endif
|
||||
!insertmacro MUI_PAGE_STARTMENU Application $AppStartMenuFolder
|
||||
|
||||
; 7. Installation page
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
|
||||
; 8. Finish page
|
||||
;
|
||||
; Don't auto jump to finish page after installation page,
|
||||
; because the installation page has useful info that can be used debug any issues with the installer.
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||
; Use show readme button in the finish page as a button create a desktop shortcut
|
||||
!define MUI_FINISHPAGE_SHOWREADME
|
||||
!define MUI_FINISHPAGE_SHOWREADME_TEXT "$(createDesktop)"
|
||||
!define MUI_FINISHPAGE_SHOWREADME_FUNCTION CreateOrUpdateDesktopShortcut
|
||||
; Show run app after installation.
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION RunMainBinary
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE SkipIfPassive
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
Function RunMainBinary
|
||||
nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" ""
|
||||
FunctionEnd
|
||||
|
||||
; Uninstaller Pages
|
||||
; 1. Confirm uninstall page
|
||||
Var DeleteAppDataCheckbox
|
||||
Var DeleteAppDataCheckboxState
|
||||
!define /ifndef WS_EX_LAYOUTRTL 0x00400000
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_SHOW un.ConfirmShow
|
||||
Function un.ConfirmShow ; Add add a `Delete app data` check box
|
||||
; $1 inner dialog HWND
|
||||
; $2 window DPI
|
||||
; $3 style
|
||||
; $4 x
|
||||
; $5 y
|
||||
; $6 width
|
||||
; $7 height
|
||||
FindWindow $1 "#32770" "" $HWNDPARENT ; Find inner dialog
|
||||
System::Call "user32::GetDpiForWindow(p r1) i .r2"
|
||||
${If} $(^RTL) = 1
|
||||
StrCpy $3 "${__NSD_CheckBox_EXSTYLE} | ${WS_EX_LAYOUTRTL}"
|
||||
IntOp $4 50 * $2
|
||||
${Else}
|
||||
StrCpy $3 "${__NSD_CheckBox_EXSTYLE}"
|
||||
IntOp $4 0 * $2
|
||||
${EndIf}
|
||||
IntOp $5 100 * $2
|
||||
IntOp $6 400 * $2
|
||||
IntOp $7 25 * $2
|
||||
IntOp $4 $4 / 96
|
||||
IntOp $5 $5 / 96
|
||||
IntOp $6 $6 / 96
|
||||
IntOp $7 $7 / 96
|
||||
System::Call 'user32::CreateWindowEx(i r3, w "${__NSD_CheckBox_CLASS}", w "$(deleteAppData)", i ${__NSD_CheckBox_STYLE}, i r4, i r5, i r6, i r7, p r1, i0, i0, i0) i .s'
|
||||
Pop $DeleteAppDataCheckbox
|
||||
SendMessage $HWNDPARENT ${WM_GETFONT} 0 0 $1
|
||||
SendMessage $DeleteAppDataCheckbox ${WM_SETFONT} $1 1
|
||||
FunctionEnd
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_LEAVE un.ConfirmLeave
|
||||
Function un.ConfirmLeave
|
||||
SendMessage $DeleteAppDataCheckbox ${BM_GETCHECK} 0 0 $DeleteAppDataCheckboxState
|
||||
FunctionEnd
|
||||
!define MUI_PAGE_CUSTOMFUNCTION_PRE un.SkipIfPassive
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
|
||||
; 2. Uninstalling Page
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
|
||||
;Languages
|
||||
{{#each languages}}
|
||||
!insertmacro MUI_LANGUAGE "{{this}}"
|
||||
{{/each}}
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
{{#each language_files}}
|
||||
!include "{{this}}"
|
||||
{{/each}}
|
||||
|
||||
Function .onInit
|
||||
${GetOptions} $CMDLINE "/P" $PassiveMode
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $PassiveMode 1
|
||||
${EndIf}
|
||||
|
||||
${GetOptions} $CMDLINE "/NS" $NoShortcutMode
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $NoShortcutMode 1
|
||||
${EndIf}
|
||||
|
||||
${GetOptions} $CMDLINE "/UPDATE" $UpdateMode
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $UpdateMode 1
|
||||
${EndIf}
|
||||
|
||||
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
|
||||
!insertmacro MUI_LANGDLL_DISPLAY
|
||||
!endif
|
||||
|
||||
!insertmacro SetContext
|
||||
|
||||
${If} $INSTDIR == "${PLACEHOLDER_INSTALL_DIR}"
|
||||
; Set default install location
|
||||
!if "${INSTALLMODE}" == "perMachine"
|
||||
${If} ${RunningX64}
|
||||
!if "${ARCH}" == "x64"
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
!else if "${ARCH}" == "arm64"
|
||||
StrCpy $INSTDIR "$PROGRAMFILES64\${PRODUCTNAME}"
|
||||
!else
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
!endif
|
||||
${Else}
|
||||
StrCpy $INSTDIR "$PROGRAMFILES\${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
!else if "${INSTALLMODE}" == "currentUser"
|
||||
StrCpy $INSTDIR "$LOCALAPPDATA\${PRODUCTNAME}"
|
||||
!endif
|
||||
|
||||
Call RestorePreviousInstallLocation
|
||||
${EndIf}
|
||||
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_INIT
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
|
||||
Section EarlyChecks
|
||||
; Abort silent installer if downgrades is disabled
|
||||
!if "${ALLOWDOWNGRADES}" == "false"
|
||||
${If} ${Silent}
|
||||
; If downgrading
|
||||
${If} $R0 = -1
|
||||
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||
${If} $0 <> 0
|
||||
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||
FileWrite $0 "$(silentDowngrades)"
|
||||
${EndIf}
|
||||
Abort
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section WebView2
|
||||
; Check if Webview2 is already installed and skip this section
|
||||
${If} ${RunningX64}
|
||||
ReadRegStr $4 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv"
|
||||
${Else}
|
||||
ReadRegStr $4 HKLM "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv"
|
||||
${EndIf}
|
||||
${If} $4 == ""
|
||||
ReadRegStr $4 HKCU "SOFTWARE\Microsoft\EdgeUpdate\Clients\${WEBVIEW2APPGUID}" "pv"
|
||||
${EndIf}
|
||||
|
||||
${If} $4 == ""
|
||||
; Webview2 installation
|
||||
;
|
||||
; Skip if updating
|
||||
${If} $UpdateMode <> 1
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "downloadBootstrapper"
|
||||
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
DetailPrint "$(webview2Downloading)"
|
||||
NSISdl::download "https://go.microsoft.com/fwlink/p/?LinkId=2124703" "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Pop $0
|
||||
${If} $0 == "success"
|
||||
DetailPrint "$(webview2DownloadSuccess)"
|
||||
${Else}
|
||||
DetailPrint "$(webview2DownloadError)"
|
||||
Abort "$(webview2AbortError)"
|
||||
${EndIf}
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "embedBootstrapper"
|
||||
Delete "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
File "/oname=$TEMP\MicrosoftEdgeWebview2Setup.exe" "${WEBVIEW2BOOTSTRAPPERPATH}"
|
||||
DetailPrint "$(installingWebview2)"
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebview2Setup.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
!if "${INSTALLWEBVIEW2MODE}" == "offlineInstaller"
|
||||
Delete "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||
File "/oname=$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe" "${WEBVIEW2INSTALLERPATH}"
|
||||
DetailPrint "$(installingWebview2)"
|
||||
StrCpy $6 "$TEMP\MicrosoftEdgeWebView2RuntimeInstaller.exe"
|
||||
Goto install_webview2
|
||||
!endif
|
||||
|
||||
Goto webview2_done
|
||||
|
||||
install_webview2:
|
||||
DetailPrint "$(installingWebview2)"
|
||||
; $6 holds the path to the webview2 installer
|
||||
ExecWait "$6 ${WEBVIEW2INSTALLERARGS} /install" $1
|
||||
${If} $1 = 0
|
||||
DetailPrint "$(webview2InstallSuccess)"
|
||||
${Else}
|
||||
DetailPrint "$(webview2InstallError)"
|
||||
Abort "$(webview2AbortError)"
|
||||
${EndIf}
|
||||
webview2_done:
|
||||
${EndIf}
|
||||
${Else}
|
||||
!if "${MINIMUMWEBVIEW2VERSION}" != ""
|
||||
${VersionCompare} "${MINIMUMWEBVIEW2VERSION}" "$4" $R0
|
||||
${If} $R0 = 1
|
||||
update_webview:
|
||||
DetailPrint "$(installingWebview2)"
|
||||
${If} ${RunningX64}
|
||||
ReadRegStr $R1 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate" "path"
|
||||
${Else}
|
||||
ReadRegStr $R1 HKLM "SOFTWARE\Microsoft\EdgeUpdate" "path"
|
||||
${EndIf}
|
||||
${If} $R1 == ""
|
||||
ReadRegStr $R1 HKCU "SOFTWARE\Microsoft\EdgeUpdate" "path"
|
||||
${EndIf}
|
||||
${If} $R1 != ""
|
||||
; 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"
|
||||
ExecWait `"$R1" /install appguid=${WEBVIEW2APPGUID}&needsadmin=true` $1
|
||||
${If} $1 = 0
|
||||
DetailPrint "$(webview2InstallSuccess)"
|
||||
${Else}
|
||||
MessageBox MB_ICONEXCLAMATION|MB_ABORTRETRYIGNORE "$(webview2InstallError)" IDIGNORE ignore IDRETRY update_webview
|
||||
Quit
|
||||
ignore:
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!endif
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Section Install
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!ifmacrodef NSIS_HOOK_PREINSTALL
|
||||
!insertmacro NSIS_HOOK_PREINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
; Copy main executable
|
||||
File "${MAINBINARYSRCPATH}"
|
||||
|
||||
; Copy resources
|
||||
{{#each resources_dirs}}
|
||||
CreateDirectory "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
{{#each resources}}
|
||||
File /a "/oname={{this.[1]}}" "{{no-escape @key}}"
|
||||
{{/each}}
|
||||
|
||||
; Copy external binaries
|
||||
{{#each binaries}}
|
||||
File /a "/oname={{this}}" "{{no-escape @key}}"
|
||||
{{/each}}
|
||||
|
||||
; Create file associations
|
||||
{{#each file_associations as |association| ~}}
|
||||
{{#each association.ext as |ext| ~}}
|
||||
!insertmacro APP_ASSOCIATE "{{ext}}" "{{or association.name ext}}" "{{association-description association.description ext}}" "$INSTDIR\${MAINBINARYNAME}.exe,0" "Open with ${PRODUCTNAME}" "$INSTDIR\${MAINBINARYNAME}.exe $\"%1$\""
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
; Register deep links
|
||||
{{#each deep_link_protocols as |protocol| ~}}
|
||||
WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "URL Protocol" ""
|
||||
WriteRegStr SHCTX "Software\Classes\\{{protocol}}" "" "URL:${BUNDLEID} protocol"
|
||||
WriteRegStr SHCTX "Software\Classes\\{{protocol}}\DefaultIcon" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\",0"
|
||||
WriteRegStr SHCTX "Software\Classes\\{{protocol}}\shell\open\command" "" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\""
|
||||
{{/each}}
|
||||
|
||||
; Create uninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Save $INSTDIR in registry for future installations
|
||||
WriteRegStr SHCTX "${MANUPRODUCTKEY}" "" $INSTDIR
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
; Save install mode to be selected by default for the next installation such as updating
|
||||
; or when uninstalling
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" $MultiUser.InstallMode 1
|
||||
!endif
|
||||
|
||||
; Remove old main binary if it doesn't match new main binary name
|
||||
ReadRegStr $OldMainBinaryName SHCTX "${UNINSTKEY}" "MainBinaryName"
|
||||
${If} $OldMainBinaryName != ""
|
||||
${AndIf} $OldMainBinaryName != "${MAINBINARYNAME}.exe"
|
||||
Delete "$INSTDIR\$OldMainBinaryName"
|
||||
${EndIf}
|
||||
|
||||
; Save current MAINBINARYNAME for future updates
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "MainBinaryName" "${MAINBINARYNAME}.exe"
|
||||
|
||||
; Registry information for add/remove programs
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayName" "${PRODUCTNAME}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayIcon" "$\"$INSTDIR\${MAINBINARYNAME}.exe$\""
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "Publisher" "${MANUFACTURER}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "InstallLocation" "$\"$INSTDIR$\""
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoModify" "1"
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "NoRepair" "1"
|
||||
|
||||
${GetSize} "$INSTDIR" "/M=uninstall.exe /S=0K /G=0" $0 $1 $2
|
||||
IntOp $0 $0 + ${ESTIMATEDSIZE}
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD SHCTX "${UNINSTKEY}" "EstimatedSize" "$0"
|
||||
|
||||
!if "${HOMEPAGE}" != ""
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "URLInfoAbout" "${HOMEPAGE}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "URLUpdateInfo" "${HOMEPAGE}"
|
||||
WriteRegStr SHCTX "${UNINSTKEY}" "HelpLink" "${HOMEPAGE}"
|
||||
!endif
|
||||
|
||||
; Create start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_WRITE_BEGIN Application
|
||||
Call CreateOrUpdateStartMenuShortcut
|
||||
!insertmacro MUI_STARTMENU_WRITE_END
|
||||
|
||||
; Create desktop shortcut for silent and passive installers
|
||||
; because finish page will be skipped
|
||||
${If} $PassiveMode = 1
|
||||
${OrIf} ${Silent}
|
||||
Call CreateOrUpdateDesktopShortcut
|
||||
${EndIf}
|
||||
|
||||
!ifmacrodef NSIS_HOOK_POSTINSTALL
|
||||
!insertmacro NSIS_HOOK_POSTINSTALL
|
||||
!endif
|
||||
|
||||
; Auto close this page for passive mode
|
||||
${If} $PassiveMode = 1
|
||||
SetAutoClose true
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Function .onInstSuccess
|
||||
; Check for `/R` flag only in silent and passive installers because
|
||||
; GUI installer has a toggle for the user to (re)start the app
|
||||
${If} $PassiveMode = 1
|
||||
${OrIf} ${Silent}
|
||||
${GetOptions} $CMDLINE "/R" $R0
|
||||
${IfNot} ${Errors}
|
||||
${GetOptions} $CMDLINE "/ARGS" $R0
|
||||
nsis_tauri_utils::RunAsUser "$INSTDIR\${MAINBINARYNAME}.exe" "$R0"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function un.onInit
|
||||
!insertmacro SetContext
|
||||
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
!insertmacro MULTIUSER_UNINIT
|
||||
!endif
|
||||
|
||||
!insertmacro MUI_UNGETLANGUAGE
|
||||
|
||||
${GetOptions} $CMDLINE "/P" $PassiveMode
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $PassiveMode 1
|
||||
${EndIf}
|
||||
|
||||
${GetOptions} $CMDLINE "/UPDATE" $UpdateMode
|
||||
${IfNot} ${Errors}
|
||||
StrCpy $UpdateMode 1
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Section Uninstall
|
||||
|
||||
!ifmacrodef NSIS_HOOK_PREUNINSTALL
|
||||
!insertmacro NSIS_HOOK_PREUNINSTALL
|
||||
!endif
|
||||
|
||||
!insertmacro CheckIfAppIsRunning "$INSTDIR\${MAINBINARYNAME}.exe" "${PRODUCTNAME}"
|
||||
|
||||
; Delete the app directory and its content from disk
|
||||
; Copy main executable
|
||||
Delete "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
|
||||
; Delete resources
|
||||
{{#each resources}}
|
||||
Delete "$INSTDIR\\{{this.[1]}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete external binaries
|
||||
{{#each binaries}}
|
||||
Delete "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
|
||||
; Delete app associations
|
||||
{{#each file_associations as |association| ~}}
|
||||
{{#each association.ext as |ext| ~}}
|
||||
!insertmacro APP_UNASSOCIATE "{{ext}}" "{{or association.name ext}}"
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
||||
; Delete deep links
|
||||
{{#each deep_link_protocols as |protocol| ~}}
|
||||
ReadRegStr $R7 SHCTX "Software\Classes\\{{protocol}}\shell\open\command" ""
|
||||
${If} $R7 == "$\"$INSTDIR\${MAINBINARYNAME}.exe$\" $\"%1$\""
|
||||
DeleteRegKey SHCTX "Software\Classes\\{{protocol}}"
|
||||
${EndIf}
|
||||
{{/each}}
|
||||
|
||||
|
||||
; Delete uninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
{{#each resources_ancestors}}
|
||||
RMDir /REBOOTOK "$INSTDIR\\{{this}}"
|
||||
{{/each}}
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
; Remove shortcuts if not updating
|
||||
${If} $UpdateMode <> 1
|
||||
!insertmacro DeleteAppUserModelId
|
||||
|
||||
; Remove start menu shortcut
|
||||
!insertmacro MUI_STARTMENU_GETFOLDER Application $AppStartMenuFolder
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
Delete "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
RMDir "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
${EndIf}
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
|
||||
; Remove desktop shortcuts
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro UnpinShortcut "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
; Remove registry information for add/remove programs
|
||||
!if "${INSTALLMODE}" == "both"
|
||||
DeleteRegKey SHCTX "${UNINSTKEY}"
|
||||
!else if "${INSTALLMODE}" == "perMachine"
|
||||
DeleteRegKey HKLM "${UNINSTKEY}"
|
||||
!else
|
||||
DeleteRegKey HKCU "${UNINSTKEY}"
|
||||
!endif
|
||||
|
||||
; Removes the Autostart entry for ${PRODUCTNAME} from the HKCU Run key if it exists.
|
||||
; This ensures the program does not launch automatically after uninstallation if it exists.
|
||||
; If it doesn't exist, it does nothing.
|
||||
; We do this when not updating (to preserve the registry value on updates)
|
||||
${If} $UpdateMode <> 1
|
||||
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${PRODUCTNAME}"
|
||||
${EndIf}
|
||||
|
||||
; Delete app data if the checkbox is selected
|
||||
; and if not updating
|
||||
${If} $DeleteAppDataCheckboxState = 1
|
||||
${AndIf} $UpdateMode <> 1
|
||||
; Clear the install location $INSTDIR from registry
|
||||
DeleteRegKey SHCTX "${MANUPRODUCTKEY}"
|
||||
DeleteRegKey /ifempty SHCTX "${MANUKEY}"
|
||||
|
||||
; Clear the install language from registry
|
||||
DeleteRegValue HKCU "${MANUPRODUCTKEY}" "Installer Language"
|
||||
DeleteRegKey /ifempty HKCU "${MANUPRODUCTKEY}"
|
||||
DeleteRegKey /ifempty HKCU "${MANUKEY}"
|
||||
|
||||
SetShellVarContext current
|
||||
RmDir /r "$APPDATA\${BUNDLEID}"
|
||||
RmDir /r "$LOCALAPPDATA\${BUNDLEID}"
|
||||
${EndIf}
|
||||
|
||||
!ifmacrodef NSIS_HOOK_POSTUNINSTALL
|
||||
!insertmacro NSIS_HOOK_POSTUNINSTALL
|
||||
!endif
|
||||
|
||||
; Auto close if passive mode or updating
|
||||
${If} $PassiveMode = 1
|
||||
${OrIf} $UpdateMode = 1
|
||||
SetAutoClose true
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Function RestorePreviousInstallLocation
|
||||
ReadRegStr $4 SHCTX "${MANUPRODUCTKEY}" ""
|
||||
StrCmp $4 "" +2 0
|
||||
StrCpy $INSTDIR $4
|
||||
FunctionEnd
|
||||
|
||||
Function Skip
|
||||
Abort
|
||||
FunctionEnd
|
||||
|
||||
Function SkipIfPassive
|
||||
${IfThen} $PassiveMode = 1 ${|} Abort ${|}
|
||||
FunctionEnd
|
||||
Function un.SkipIfPassive
|
||||
${IfThen} $PassiveMode = 1 ${|} Abort ${|}
|
||||
FunctionEnd
|
||||
|
||||
Function CreateOrUpdateStartMenuShortcut
|
||||
; We used to use product name as MAINBINARYNAME
|
||||
; migrate old shortcuts to target the new MAINBINARYNAME
|
||||
StrCpy $R0 0
|
||||
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro SetShortcutTarget "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
|
||||
!insertmacro IsShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro SetShortcutTarget "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
StrCpy $R0 1
|
||||
${EndIf}
|
||||
|
||||
${If} $R0 = 1
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
; Skip creating shortcut if in update mode or no shortcut mode
|
||||
; but always create if migrating from wix
|
||||
${If} $WixMode = 0
|
||||
${If} $UpdateMode = 1
|
||||
${OrIf} $NoShortcutMode = 1
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
!if "${STARTMENUFOLDER}" != ""
|
||||
CreateDirectory "$SMPROGRAMS\$AppStartMenuFolder"
|
||||
CreateShortcut "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\$AppStartMenuFolder\${PRODUCTNAME}.lnk"
|
||||
!else
|
||||
CreateShortcut "$SMPROGRAMS\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
!insertmacro SetLnkAppUserModelId "$SMPROGRAMS\${PRODUCTNAME}.lnk"
|
||||
!endif
|
||||
FunctionEnd
|
||||
|
||||
Function CreateOrUpdateDesktopShortcut
|
||||
; We used to use product name as MAINBINARYNAME
|
||||
; migrate old shortcuts to target the new MAINBINARYNAME
|
||||
!insertmacro IsShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\$OldMainBinaryName"
|
||||
Pop $0
|
||||
${If} $0 = 1
|
||||
!insertmacro SetShortcutTarget "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
Return
|
||||
${EndIf}
|
||||
|
||||
; Skip creating shortcut if in update mode or no shortcut mode
|
||||
; but always create if migrating from wix
|
||||
${If} $WixMode = 0
|
||||
${If} $UpdateMode = 1
|
||||
${OrIf} $NoShortcutMode = 1
|
||||
Return
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
CreateShortcut "$DESKTOP\${PRODUCTNAME}.lnk" "$INSTDIR\${MAINBINARYNAME}.exe"
|
||||
!insertmacro SetLnkAppUserModelId "$DESKTOP\${PRODUCTNAME}.lnk"
|
||||
FunctionEnd
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_ARABIC} "إضافة أو إزالة المكونات"
|
||||
LangString alreadyInstalled ${LANG_ARABIC} "التطبيق مثبت بالفعل"
|
||||
LangString alreadyInstalledLong ${LANG_ARABIC} "${PRODUCTNAME} ${VERSION} مثبت بالفعل. قم باختيار العملية التى تريدها ثم اضغط على التالى."
|
||||
LangString appRunning ${LANG_ARABIC} "{{product_name}} مازال يعمل! من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString appRunningOkKill ${LANG_ARABIC} "{{product_name}} مازال يعمل!$\nاضغط OK لإغلاقه"
|
||||
LangString chooseMaintenanceOption ${LANG_ARABIC} "قم باختيار نوع الصيانة التى تريدها."
|
||||
LangString choowHowToInstall ${LANG_ARABIC} "قم باختيار طريقة تنصيب ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ARABIC} "اضف اختصار على سطح المكتب"
|
||||
LangString dontUninstall ${LANG_ARABIC} "عدم إزالة"
|
||||
LangString dontUninstallDowngrade ${LANG_ARABIC} "عدم إزالة (التخفيض بدون إزالة غير مسموح لهذا المثبت)"
|
||||
LangString failedToKillApp ${LANG_ARABIC} "فشل فى غلف {{product_name}}. من فضلك، قم بإغلاق التطبيق أولاً ثم حاول مرة أخرى."
|
||||
LangString installingWebview2 ${LANG_ARABIC} "تنصيب WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ARABIC} "يوجد نسخة جديدة من ${PRODUCTNAME} مثبتة بالغعل! لا ينصح بتنصيب نسخة اقدم من النسخة الحالية. اذا مازلت ترغب فى تنصيب النسخة الأقدم، فينصح بإزالة النسخة الحالية أولاً. قم باختيار العملية التى تريدها ثم اضغط على التالى للاستمرار."
|
||||
LangString older ${LANG_ARABIC} "أقدم"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_ARABIC} "نسخة $R4 من ${PRODUCTNAME} مثبتة بالفعل على نظامك. ينصح بإزالة النسخة الحالية قبل التنصيب. قم باختيار العملية التى تريدها ثم اضغط على التالى للاستمرار."
|
||||
LangString silentDowngrades ${LANG_ARABIC} "التخفيض غير مسموح لهذا المثبت ولا يمكن الاستمرار فى الوضع الصامت. من فضلك، قم باستخدام الواجهة الرسومية."
|
||||
LangString unableToUninstall ${LANG_ARABIC} "غير قادر على الإزالة!"
|
||||
LangString uninstallApp ${LANG_ARABIC} "إزالة ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_ARABIC} "قم بإزالة التطبيق قبل التثبيت"
|
||||
LangString unknown ${LANG_ARABIC} "غير معروفة"
|
||||
LangString webview2AbortError ${LANG_ARABIC} "فشل فى تنصيب WebView2! لا يمكن تشغيل التطبيق من غيره. من فضلك، حاول إعادة تشغيل المثبت."
|
||||
LangString webview2DownloadError ${LANG_ARABIC} "خطأ: فشل تنزيل WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_ARABIC} "تم تنزيل WebView2 bootstrapper بنجاح"
|
||||
LangString webview2Downloading ${LANG_ARABIC} "يتم تنزيل WebView2 bootstrapper..."
|
||||
LangString webview2InstallError ${LANG_ARABIC} "خطأ: فشل فى تنصيب WebView2 بكود $1"
|
||||
LangString webview2InstallSuccess ${LANG_ARABIC} "تم تنصيب WebView2 بنجاح"
|
||||
LangString deleteAppData ${LANG_ARABIC} "مسح بيانات التطبيق"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_BULGARIAN} "Добавяне/Преинсталиране на компоненти"
|
||||
LangString alreadyInstalled ${LANG_BULGARIAN} "Вече инсталиран"
|
||||
LangString alreadyInstalledLong ${LANG_BULGARIAN} "${PRODUCTNAME} ${VERSION} е вече е инсталиран. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите."
|
||||
LangString appRunning ${LANG_BULGARIAN} "{{product_name}} е отворен! Моля, затворете го първо и опитайте отново."
|
||||
LangString appRunningOkKill ${LANG_BULGARIAN} "{{product_name}} е отворен!$\nНатиснете ОК, за да го затворите."
|
||||
LangString chooseMaintenanceOption ${LANG_BULGARIAN} "Изберете опция за поддръжка."
|
||||
LangString choowHowToInstall ${LANG_BULGARIAN} "Изберете как искате да инсталирате ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_BULGARIAN} "Създайте пряк път на работния плот"
|
||||
LangString dontUninstall ${LANG_BULGARIAN} "Не деинсталирайте"
|
||||
LangString dontUninstallDowngrade ${LANG_BULGARIAN} "Не деинсталирайте (Понижаването без деинсталация е забранено за този инсталатор)"
|
||||
LangString failedToKillApp ${LANG_BULGARIAN} "Неуспешно прекратяване на {{product_name}}. Моля, затворете го първо и опитайте отново."
|
||||
LangString installingWebview2 ${LANG_BULGARIAN} "Инсталиране на WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_BULGARIAN} "Вече е инсталирана по-нова версия на ${PRODUCTNAME}! Не се препоръчва да инсталирате по-стара версия. Ако наистина желаете да инсталирате тази по-стара версия, по-добре е да деинсталирате текущата версия първо. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите."
|
||||
LangString older ${LANG_BULGARIAN} "по-стара"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_BULGARIAN} "На вашата система е инсталирана $R4 версия на ${PRODUCTNAME}. Препоръчително е да деинсталирате текущата версия преди да инсталирате нова. Изберете операцията, която искате да извършите и натиснете Напред, за да продължите."
|
||||
LangString silentDowngrades ${LANG_BULGARIAN} "Понижаванията не са позволени за този инсталатор. Не е възможно да продължите с безшумен инсталатор. Моля, използвайте графичния интерфейсен инсталатор."
|
||||
LangString unableToUninstall ${LANG_BULGARIAN} "Неуспешна деинсталация!"
|
||||
LangString uninstallApp ${LANG_BULGARIAN} "Деинсталиране на ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_BULGARIAN} "Деинсталирай преди инсталиране"
|
||||
LangString unknown ${LANG_BULGARIAN} "неизвестно"
|
||||
LangString webview2AbortError ${LANG_BULGARIAN} "Неуспешно инсталиране на WebView2! Приложението не може да работи без него. Опитайте да рестартирате инсталатора."
|
||||
LangString webview2DownloadError ${LANG_BULGARIAN} "Грешка: Неуспешно изтегляне на WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_BULGARIAN} "Стартиращият файл на WebView2 е изтеглен успешно"
|
||||
LangString webview2Downloading ${LANG_BULGARIAN} "Изтегляне на стартиращят файл на WebView2..."
|
||||
LangString webview2InstallError ${LANG_BULGARIAN} "Грешка: Инсталирането на WebView2 неуспешно с код на изход $1"
|
||||
LangString webview2InstallSuccess ${LANG_BULGARIAN} "WebView2 инсталиран успешно"
|
||||
LangString deleteAppData ${LANG_BULGARIAN} "Изтриване на данните на приложението"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_DUTCH} "(Her)installeer componenten"
|
||||
LangString alreadyInstalled ${LANG_DUTCH} "Al geïnstalleerd"
|
||||
LangString alreadyInstalledLong ${LANG_DUTCH} "${PRODUCTNAME} ${VERSION} is al geïnstalleerd. Kies een van de volgende opties en klik op Volgende om door te gaan."
|
||||
LangString appRunning ${LANG_DUTCH} "{{product_name}} is geopend! Sluit het programma eerst en probeer het dan opnieuw."
|
||||
LangString appRunningOkKill ${LANG_DUTCH} "{{product_name}} is geopend!$\nKlik op OK om het te stoppen."
|
||||
LangString chooseMaintenanceOption ${LANG_DUTCH} "Kies de onderhoudsoptie die u wilt uitvoeren."
|
||||
LangString choowHowToInstall ${LANG_DUTCH} "Kies hoe u ${PRODUCTNAME} wilt installeren."
|
||||
LangString createDesktop ${LANG_DUTCH} "Maak een snelkoppeling aan op het bureaublad"
|
||||
LangString dontUninstall ${LANG_DUTCH} "Deïnstalleer niet"
|
||||
LangString dontUninstallDowngrade ${LANG_DUTCH} "Deïnstalleer niet (Downgraden zonder deïnstalleren is uitgeschakeld voor deze installer)"
|
||||
LangString failedToKillApp ${LANG_DUTCH} "Het is niet gelukt {{product_name}} te stoppen. Sluit het eerst zelf en probeer het dan nog een keer"
|
||||
LangString installingWebview2 ${LANG_DUTCH} "WebView2 wordt geïnstalleerd..."
|
||||
LangString newerVersionInstalled ${LANG_DUTCH} "Een nieuwere versie van ${PRODUCTNAME} is al geïnstalleerd! Het word niet aangeraden om een oudere versie te installeren. Als u echt deze oudere versie wilt installeren, kunt u beter de huidige versie eerst deïnstalleren. Kies een van de volgende opties en klik op Volgende om door te gaan."
|
||||
LangString older ${LANG_DUTCH} "oudere"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_DUTCH} "Een $R4 versie van ${PRODUCTNAME} is al geïnstalleerd. Het word aangeraden om de huidige versie eerst te deïnstalleren. Kies een van de volgende opties en klik op Volgende om door te gaan."
|
||||
LangString silentDowngrades ${LANG_DUTCH} "Downgrades zijn uitgeschakeld voor deze installer, de stille installatie kan niet worden voltooid, gebruik a.u.b. de grafische installatie methode.$\n"
|
||||
LangString unableToUninstall ${LANG_DUTCH} "De installatie kan niet ongedaan worden gemaakt."
|
||||
LangString uninstallApp ${LANG_DUTCH} "Deïnstalleer ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_DUTCH} "Deïnstalleer voor installatie"
|
||||
LangString unknown ${LANG_DUTCH} "onbekende"
|
||||
LangString webview2AbortError ${LANG_DUTCH} "De installatie van WebView2 is mislukt! De software kan niet draaien zonder. Probeer de installatie opnieuw te starten."
|
||||
LangString webview2DownloadError ${LANG_DUTCH} "Error: Het downloaden van WebView2 is mislukt - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_DUTCH} "De download van WebView2 bootstrapper is gelukt"
|
||||
LangString webview2Downloading ${LANG_DUTCH} "WebView2 bootstrapper aan het downloaden..."
|
||||
LangString webview2InstallError ${LANG_DUTCH} "Error: Het installeren van WebView2 is mislukt met exit-code $1"
|
||||
LangString webview2InstallSuccess ${LANG_DUTCH} "De installatie van WebView2 is gelukt"
|
||||
LangString deleteAppData ${LANG_DUTCH} "Verwijder de data van de applicatie"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_ENGLISH} "Add/Reinstall components"
|
||||
LangString alreadyInstalled ${LANG_ENGLISH} "Already Installed"
|
||||
LangString alreadyInstalledLong ${LANG_ENGLISH} "${PRODUCTNAME} ${VERSION} is already installed. Select the operation you want to perform and click Next to continue."
|
||||
LangString appRunning ${LANG_ENGLISH} "{{product_name}} is running! Please close it first then try again."
|
||||
LangString appRunningOkKill ${LANG_ENGLISH} "{{product_name}} is running!$\nClick OK to kill it"
|
||||
LangString chooseMaintenanceOption ${LANG_ENGLISH} "Choose the maintenance option to perform."
|
||||
LangString choowHowToInstall ${LANG_ENGLISH} "Choose how you want to install ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ENGLISH} "Create desktop shortcut"
|
||||
LangString dontUninstall ${LANG_ENGLISH} "Do not uninstall"
|
||||
LangString dontUninstallDowngrade ${LANG_ENGLISH} "Do not uninstall (Downgrading without uninstall is disabled for this installer)"
|
||||
LangString failedToKillApp ${LANG_ENGLISH} "Failed to kill {{product_name}}. Please close it first then try again"
|
||||
LangString installingWebview2 ${LANG_ENGLISH} "Installing WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ENGLISH} "A newer version of ${PRODUCTNAME} is already installed! It is not recommended that you install an older version. If you really want to install this older version, it's better to uninstall the current version first. Select the operation you want to perform and click Next to continue."
|
||||
LangString older ${LANG_ENGLISH} "older"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_ENGLISH} "An $R4 version of ${PRODUCTNAME} is installed on your system. It's recommended that you uninstall the current version before installing. Select the operation you want to perform and click Next to continue."
|
||||
LangString silentDowngrades ${LANG_ENGLISH} "Downgrades are disabled for this installer, can't proceed with the silent installer, please use the graphical interface installer instead.$\n"
|
||||
LangString unableToUninstall ${LANG_ENGLISH} "Unable to uninstall!"
|
||||
LangString uninstallApp ${LANG_ENGLISH} "Uninstall ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_ENGLISH} "Uninstall before installing"
|
||||
LangString unknown ${LANG_ENGLISH} "unknown"
|
||||
LangString webview2AbortError ${LANG_ENGLISH} "Failed to install WebView2! The app can't run without it. Try restarting the installer."
|
||||
LangString webview2DownloadError ${LANG_ENGLISH} "Error: Downloading WebView2 Failed - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_ENGLISH} "WebView2 bootstrapper downloaded successfully"
|
||||
LangString webview2Downloading ${LANG_ENGLISH} "Downloading WebView2 bootstrapper..."
|
||||
LangString webview2InstallError ${LANG_ENGLISH} "Error: Installing WebView2 failed with exit code $1"
|
||||
LangString webview2InstallSuccess ${LANG_ENGLISH} "WebView2 installed successfully"
|
||||
LangString deleteAppData ${LANG_ENGLISH} "Delete the application data"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_FRENCH} "Ajouter/Réinstaller un composant."
|
||||
LangString alreadyInstalled ${LANG_FRENCH} "Déja installé."
|
||||
LangString alreadyInstalledLong ${LANG_FRENCH} "${PRODUCTNAME} ${VERSION} est déja installé. Sélectionnez l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continuer."
|
||||
LangString appRunning ${LANG_FRENCH} "{{product_name}} est en cours d'exécution. Veuillez fermer l'application avant de réessayer."
|
||||
LangString appRunningOkKill ${LANG_FRENCH} "{{product_name}} est en cours d'exécution.$\nCliquez sur OK pour fermer l'application."
|
||||
LangString chooseMaintenanceOption ${LANG_FRENCH} "Veuillez choisir l'option de maintenance à effectuer."
|
||||
LangString choowHowToInstall ${LANG_FRENCH} "Veuillez choisir l'emplacement d'installation de ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_FRENCH} "Créer un raccourci sur le bureau."
|
||||
LangString dontUninstall ${LANG_FRENCH} "Ne pas désinstaller"
|
||||
LangString dontUninstallDowngrade ${LANG_FRENCH} "Ne pas désinstaller (revenir à une ancienne version sans désinstallation est désactivé pour cet installateur)"
|
||||
LangString failedToKillApp ${LANG_FRENCH} "La fermeture de {{product_name}} a échoué. Veuillez fermer l'application et réessayer."
|
||||
LangString installingWebview2 ${LANG_FRENCH} "Installation de WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_FRENCH} "Une version plus récente de ${PRODUCTNAME} est déja installée. Il n'est pas recommandé d'installer une ancienne version. Si vous souhaitez installer cette ancienne version, il est conseillé de désinstaller la version courante en premier. Veuillez sélectionner l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continer."
|
||||
LangString older ${LANG_FRENCH} "ancien"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_FRENCH} "La version $R4 de ${PRODUCTNAME} est installée sur le système. Il est recommandé de désinstaller la version actuelle avant d'installer celle-ci. Sélectionnez l'opération que vous souhaitez effectuer, puis cliquez sur Suivant pour continuer."
|
||||
LangString silentDowngrades ${LANG_FRENCH} "Revenir à une version antérieure est désactivé pour cet installateur. Impossible de continuer avec l'installation silencieuse, veuillez utiliser l'interface graphique à la place.$\n"
|
||||
LangString unableToUninstall ${LANG_FRENCH} "Impossible de désinstaller le programme !"
|
||||
LangString uninstallApp ${LANG_FRENCH} "Désinstaller ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_FRENCH} "Désinstaller avant d'installer"
|
||||
LangString unknown ${LANG_FRENCH} "inconnu"
|
||||
LangString webview2AbortError ${LANG_FRENCH} "L'installation de WebView2 a échoué ! L'application ne peut s'éxécuter sans WebView2. Veuillez essayer de redémarrer l'installation."
|
||||
LangString webview2DownloadError ${LANG_FRENCH} "Erreur : le téléchargement de WebView2 a échoué - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_FRENCH} "Le composant WebView2 a été téléchargé avec succès !"
|
||||
LangString webview2Downloading ${LANG_FRENCH} "Téléchargement du composant WebView2..."
|
||||
LangString webview2InstallError ${LANG_FRENCH} "Erreur : l'installation de WebView2 a échoué avec le code d'erreur $1"
|
||||
LangString webview2InstallSuccess ${LANG_FRENCH} "L'installation de WebView2 a réussi"
|
||||
LangString deleteAppData ${LANG_FRENCH} "Supprimer les données de l'application"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_GERMAN} "Komponenten hinzufügen/neu installieren"
|
||||
LangString alreadyInstalled ${LANG_GERMAN} "Bereits installiert"
|
||||
LangString alreadyInstalledLong ${LANG_GERMAN} "${PRODUCTNAME} ${VERSION} ist bereits installiert. Wählen Sie den gewünschten Vorgang aus und klicken Sie auf Weiter, um fortzufahren."
|
||||
LangString appRunning ${LANG_GERMAN} "{{product_name}} wird ausgeführt! Bitte schließen Sie es zuerst und versuchen Sie es dann erneut."
|
||||
LangString appRunningOkKill ${LANG_GERMAN} "{{product_name}} läuft! $\nKlicken Sie auf OK, um es zu beenden"
|
||||
LangString chooseMaintenanceOption ${LANG_GERMAN} "Wählen Sie die auszuführende Wartungsoption."
|
||||
LangString choowHowToInstall ${LANG_GERMAN} "Wählen Sie, wie Sie ${PRODUCTNAME} installieren möchten."
|
||||
LangString createDesktop ${LANG_GERMAN} "Desktop-Verknüpfung erstellen"
|
||||
LangString dontUninstall ${LANG_GERMAN} "Nicht deinstallieren"
|
||||
LangString dontUninstallDowngrade ${LANG_GERMAN} "Nicht deinstallieren (Downgrading ohne Deinstallation ist für dieses Installationsprogramm deaktiviert)"
|
||||
LangString failedToKillApp ${LANG_GERMAN} "Failed to kill {{product_name}}. Bitte schließen Sie es zuerst und versuchen Sie es dann erneut"
|
||||
LangString installingWebview2 ${LANG_GERMAN} "Installiere WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_GERMAN} "Eine neuere Version von ${PRODUCTNAME} ist bereits installiert! Es wird nicht empfohlen, eine ältere Version zu installieren. Wenn Sie diese ältere Version wirklich installieren wollen, ist es besser, die aktuelle Version zuerst zu deinstallieren. Wählen Sie den gewünschten Vorgang aus und klicken Sie auf Weiter, um fortzufahren."
|
||||
LangString älter ${LANG_GERMAN} "älter"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_GERMAN} "Eine $R4-Version von ${PRODUCTNAME} ist auf Ihrem System installiert. Es wird empfohlen, dass Sie die aktuelle Version vor der Installation deinstallieren. Wählen Sie den gewünschten Vorgang aus und klicken Sie auf Weiter, um fortzufahren."
|
||||
LangString silentDowngrades ${LANG_GERMAN} "Downgrades sind für dieses Installationsprogramm deaktiviert, Sie können nicht mit dem Silent-Installationsprogramm fortfahren, bitte verwenden Sie stattdessen das Installationsprogramm mit grafischer Benutzeroberfläche.$\n"
|
||||
LangString unableToUninstall ${LANG_GERMAN} "Unable to uninstall!"
|
||||
LangString uninstallApp ${LANG_GERMAN} "Deinstalliere ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_GERMAN} "Vor der Installation deinstallieren"
|
||||
LangString unbekannt ${LANG_GERMAN} "unbekannt"
|
||||
LangString webview2AbortError ${LANG_GERMAN} "Die Installation von WebView2 ist fehlgeschlagen! Die Anwendung kann ohne es nicht laufen. Versuchen Sie, das Installationsprogramm neu zu starten."
|
||||
LangString webview2DownloadError ${LANG_GERMAN} "Fehler: Herunterladen von WebView2 fehlgeschlagen - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_GERMAN} "WebView2 Bootstrapper erfolgreich heruntergeladen"
|
||||
LangString webview2Downloading ${LANG_GERMAN} "Herunterladen des WebView2 Bootstrappers..."
|
||||
LangString webview2InstallError ${LANG_GERMAN} "Fehler: Die Installation von WebView2 ist mit Exit Code $1 fehlgeschlagen"
|
||||
LangString webview2InstallSuccess ${LANG_GERMAN} "WebView2 erfolgreich installiert"
|
||||
LangString deleteAppData ${LANG_GERMAN} "Lösche die Anwendungsdaten"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_HEBREW} "הוסף או התקן מחדש"
|
||||
LangString alreadyInstalled ${LANG_HEBREW} "כבר מותקן"
|
||||
LangString alreadyInstalledLong ${LANG_HEBREW} "${PRODUCTNAME} ${VERSION} כבר מותקן. בחר את הפעולה שברצונך לבצע ולחץ על הבא כדי להמשיך."
|
||||
LangString appRunning ${LANG_HEBREW} "{{product_name}} פועל! נא לסגור אותו ולנסות שוב."
|
||||
LangString appRunningOkKill ${LANG_HEBREW} "{{product_name}} פועל!$\nלחץ אישור כדי לסגור אותו."
|
||||
LangString chooseMaintenanceOption ${LANG_HEBREW} "בחר את פעולת התחזוקה לביצוע"
|
||||
LangString choowHowToInstall ${LANG_HEBREW} "בחר איך תרצה להתקין את ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_HEBREW} "צור קיצור דרך בשולחן העבודה"
|
||||
LangString dontUninstall ${LANG_HEBREW} "אל תסיר"
|
||||
LangString dontUninstallDowngrade ${LANG_HEBREW} "אל תסיר (התקנת גרסה ישנה ללא הסרת הגרסה הנוכחית מושעית עבור התקנה זו)"
|
||||
LangString failedToKillApp ${LANG_HEBREW} "עצירת {{product_name}} נכשלה. נא לסגור את היישום ולנסות שוב."
|
||||
LangString installingWebview2 ${LANG_HEBREW} "מתקין את WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_HEBREW} "גרסה חדשה יותר של ${PRODUCTNAME} כבר מותקנת! לא מומלץ להתקין גרסה ישנה. אם בכל זאת תרצה להתקין את הגרסה הזו, מומלץ קודם להסיר את הגרסה הנוכחית. בחר את הפעולה שברצונך לבצע ולחץ הבא להמשך."
|
||||
LangString older ${LANG_HEBREW} "ישנה"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_HEBREW} "גרסה $R4 של ${PRODUCTNAME} מותקנת במערכת שלך. מומלץ להסיר את הגרסה הנוכחית לפני ההתקנה. בחר את הפעולה שברצונך לבצע ולחץ הבא להמשך."
|
||||
LangString silentDowngrades ${LANG_HEBREW} "התקנת גרסה ישנה לא נתמכת בהתקנה זו, אין אפשרות להמשיך עם ההתקנה השקטה, נא להמשיך עם ההתקנה בממשק הגרפי.$\n"
|
||||
LangString unableToUninstall ${LANG_HEBREW} "לא ניתן להסיר!"
|
||||
LangString uninstallApp ${LANG_HEBREW} "הסר את ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_HEBREW} "הסר את הגרסה הנוכחית לפני התקנת גרסה זו"
|
||||
LangString unknown ${LANG_HEBREW} "לא ידועה"
|
||||
LangString webview2AbortError ${LANG_HEBREW} "התקנת WebView2 נכשלה! היישום אינו יכולה לפעול בלי זה. נסה להפעיל את ההתקנה שוב."
|
||||
LangString webview2DownloadError ${LANG_HEBREW} "שגיאה: הורדת WebView2 נכשלה - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_HEBREW} "מאתחל WebView2 הורד בהצלחה"
|
||||
LangString webview2Downloading ${LANG_HEBREW} "מוריד את מאתחל WebView2..."
|
||||
LangString webview2InstallError ${LANG_HEBREW} "שגיאה: התקנת WebView2 נכשלה עם קוד שגיאה $1"
|
||||
LangString webview2InstallSuccess ${LANG_HEBREW} "WebView2 הותקן בהצלחה"
|
||||
LangString deleteAppData ${LANG_HEBREW} "מחק את נתוני היישום"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_ITALIAN} "Aggiungi/reinstalla componenti"
|
||||
LangString alreadyInstalled ${LANG_ITALIAN} "Programma già installato"
|
||||
LangString alreadyInstalledLong ${LANG_ITALIAN} "${PRODUCTNAME} ${VERSION} è già installato.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'."
|
||||
LangString appRunning ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nChiudi {product_name}} e riprova."
|
||||
LangString appRunningOkKill ${LANG_ITALIAN} "{{product_name}} è in esecuzione!$\nPer chiudere {product_name} seleziona 'OK'"
|
||||
LangString chooseMaintenanceOption ${LANG_ITALIAN} "Scegli l'operazione di manutenzione da eseguire."
|
||||
LangString choowHowToInstall ${LANG_ITALIAN} "Scegli come vuoi installare ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_ITALIAN} "Crea collegamento sul desktop"
|
||||
LangString dontUninstall ${LANG_ITALIAN} "Non disinstallare"
|
||||
LangString dontUninstallDowngrade ${LANG_ITALIAN} "Non disinstallare (per questo installer il downgrade senza la disinstallazione è disabilitato)"
|
||||
LangString failedToKillApp ${LANG_ITALIAN} "Impossibile chiudere {{product_name}}.$\nChiudi {product_name} e poi riprova"
|
||||
LangString installingWebview2 ${LANG_ITALIAN} "Installazione WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_ITALIAN} "È già installata una versione più recente di ${PRODUCTNAME}!$\nNon è consigliato installare una versione più vecchia.$\nSe vuoi comunque procedere, è meglio prima disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'."
|
||||
LangString older ${LANG_ITALIAN} "più vecchia"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_ITALIAN} "Nel sistema è installata una versione $R4 di ${PRODUCTNAME}.$\nPrima di procedere all'installazione è consigliabile disinstallare la versione attuale.$\nPer continuare scegli l'operazione da eseguire e seleziona 'Avanti'."
|
||||
LangString silentDowngrades ${LANG_ITALIAN} "Per questo installer i downgrade sono disabilitati , impossibile procedere con l'installer silenzioso, usa invece l'installer con interfaccia grafica.$\n"
|
||||
LangString unableToUninstall ${LANG_ITALIAN} "Impossibile disinstallare!"
|
||||
LangString uninstallApp ${LANG_ITALIAN} "Disinstalla ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_ITALIAN} "Disinstalla prima di installare"
|
||||
LangString unknown ${LANG_ITALIAN} "sconosciuta"
|
||||
LangString webview2AbortError ${LANG_ITALIAN} "Errore nell'installazione di WebView2!$\nL'app non può funzionare senza.$\nProva a riavviare l'installer."
|
||||
LangString webview2DownloadError ${LANG_ITALIAN} "Errore: il download di WebView2 è fallito - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_ITALIAN} "Download bootstrapper WebView2 completato"
|
||||
LangString webview2Downloading ${LANG_ITALIAN} "Download bootstrapper WebView2..."
|
||||
LangString webview2InstallError ${LANG_ITALIAN} "Errore: l'installazione di WebView2 è fallita con codice errore $1"
|
||||
LangString webview2InstallSuccess ${LANG_ITALIAN} "WebView2 installato correttamente"
|
||||
LangString deleteAppData ${LANG_ITALIAN} "Cancella i dati dell'applicazione"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_JAPANESE} "コンポーネントの追加・再インストール"
|
||||
LangString alreadyInstalled ${LANG_JAPANESE} "既にインストールされています"
|
||||
LangString alreadyInstalledLong ${LANG_JAPANESE} "${PRODUCTNAME} ${VERSION} は既にインストールされています。実行したい操作を選択し、「次へ」をクリックして続行します。"
|
||||
LangString appRunning ${LANG_JAPANESE} "{{product_name}} は動作中です。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString appRunningOkKill ${LANG_JAPANESE} "{{product_name}} は動作中です。$\n「OK」を押すと動作中のプログラムを終了します。"
|
||||
LangString chooseMaintenanceOption ${LANG_JAPANESE} "メンテナンスオプションを選択して実行します。"
|
||||
LangString choowHowToInstall ${LANG_JAPANESE} "${PRODUCTNAME} のインストール方法を選択してください。"
|
||||
LangString createDesktop ${LANG_JAPANESE} "デスクトップショートカットを作成する"
|
||||
LangString dontUninstall ${LANG_JAPANESE} "アンインストールしない"
|
||||
LangString dontUninstallDowngrade ${LANG_JAPANESE} "アンインストールしない (このインストーラーでは、アンインストールをせずにダウングレードすることはできません)"
|
||||
LangString failedToKillApp ${LANG_JAPANESE} "{{product_name}} の終了に失敗しました。動作中のプログラムを終了し、もう一度やり直してください。"
|
||||
LangString installingWebview2 ${LANG_JAPANESE} "WebView2 をインストール中です..."
|
||||
LangString newerVersionInstalled ${LANG_JAPANESE} "既に新しいバージョンの ${PRODUCTNAME} がインストールされています。古いバージョンをインストールすることは推奨されません。どうしてもこの旧バージョンをインストールしたい場合は、先に現行バージョンをアンインストールしておく方がよいでしょう。実行したい操作を選択し、「次へ」をクリックして続行します。"
|
||||
LangString older ${LANG_JAPANESE} "旧"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_JAPANESE} "お使いのシステムには、 ${PRODUCTNAME} のバージョン $R4 がインストールされています。インストールする前に、現在のバージョンをアンインストールすることをお勧めします。実行したい操作を選択し、「次へ」をクリックして続行します。"
|
||||
LangString silentDowngrades ${LANG_JAPANESE} "このインストーラーではダウングレードはできません。サイレントインストーラーを続行できないので、代わりにグラフィカルインターフェースインストーラーを使用してください。$\n"
|
||||
LangString unableToUninstall ${LANG_JAPANESE} "アンインストールできません。"
|
||||
LangString uninstallApp ${LANG_JAPANESE} "${PRODUCTNAME} をアンインストールする"
|
||||
LangString uninstallBeforeInstalling ${LANG_JAPANESE} "インストールする前にアンインストールする"
|
||||
LangString unknown ${LANG_JAPANESE} "不明"
|
||||
LangString webview2AbortError ${LANG_JAPANESE} "WebView2 のインストールに失敗しました。 WebView2 がないとアプリは実行できません。インストーラーを再起動してください。"
|
||||
LangString webview2DownloadError ${LANG_JAPANESE} "エラー: WebView2 のダウンロードに失敗しました - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_JAPANESE} "WebView2 ブートストラップ が正常にダウンロードされました"
|
||||
LangString webview2Downloading ${LANG_JAPANESE} "WebView2 ブートストラップ をダウンロード中です..."
|
||||
LangString webview2InstallError ${LANG_JAPANESE} "エラー: WebView2 のインストールは終了コード $1 で失敗しました。"
|
||||
LangString webview2InstallSuccess ${LANG_JAPANESE} "WebView2 が正常にインストールされました"
|
||||
LangString deleteAppData ${LANG_JAPANESE} "アプリケーションデータを削除する"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_KOREAN} "컴포넌트 추가 및 재설치"
|
||||
LangString alreadyInstalled ${LANG_KOREAN} "이미 설치되어 있습니다"
|
||||
LangString alreadyInstalledLong ${LANG_KOREAN} "${PRODUCTNAME} ${VERSION}이(가) 이미 설치되어 있습니다. 수행하고자 하는 작업을 선택하고 '다음'을 클릭하여 계속합니다."
|
||||
LangString appRunning ${LANG_KOREAN} "{{product_name}}이(가) 실행 중입니다! 먼저 닫은 후 다시 시도하세요."
|
||||
LangString appRunningOkKill ${LANG_KOREAN} "{{product_name}}이(가) 실행 중입니다!$\n'OK'를 누르면 실행 중인 프로그램을 종료합니다."
|
||||
LangString chooseMaintenanceOption ${LANG_KOREAN} "수행하려는 관리 옵션을 선택합니다."
|
||||
LangString choowHowToInstall ${LANG_KOREAN} "${PRODUCTNAME}의 설치 방법을 선택하세요.."
|
||||
LangString createDesktop ${LANG_KOREAN} "바탕화면 바로가기 만들기"
|
||||
LangString dontUninstall ${LANG_KOREAN} "제거하지 않기"
|
||||
LangString dontUninstallDowngrade ${LANG_KOREAN} "제거하지 않기 (이 설치 프로그램에서는 제거하지 않고 다운그레이드할 수 없습니다.)"
|
||||
LangString failedToKillApp ${LANG_KOREAN} "{{product_name}}을(를) 종료하지 못했습니다. 먼저 닫은 후 다시 시도하세요."
|
||||
LangString installingWebview2 ${LANG_KOREAN} "WebView2를 설치하는 중입니다..."
|
||||
LangString newerVersionInstalled ${LANG_KOREAN} "${PRODUCTNAME}의 최신 버전이 이미 설치되어 있습니다! 이전 버전을 설치하지 않는 것이 좋습니다. 이 이전 버전을 꼭 설치하려면 먼저 현재 버전을 제거하는 것이 좋습니다. 수행하려는 작업을 선택하고 '다음'을 클릭하여 계속합니다."
|
||||
LangString older ${LANG_KOREAN} "구"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_KOREAN} "시스템에 ${PRODUCTNAME}의 $R4 버전이 설치되어 있습니다. 설치하기 전에 현재 버전을 제거하는 것이 좋습니다. 수행하려는 작업을 선택하고 다음을 클릭하여 계속합니다."
|
||||
LangString silentDowngrades ${LANG_KOREAN} "이 설치 프로그램에서는 다운그레이드가 비활성화되어 자동 설치 프로그램을 진행할 수 없습니다. 대신 그래픽 인터페이스 설치 프로그램을 사용하세요.$\n"
|
||||
LangString unableToUninstall ${LANG_KOREAN} "제거할 수 없습니다!"
|
||||
LangString uninstallApp ${LANG_KOREAN} "${PRODUCTNAME} 제거하기"
|
||||
LangString uninstallBeforeInstalling ${LANG_KOREAN} "설치하기 전에 제거하기"
|
||||
LangString unknown ${LANG_KOREAN} "알 수 없음"
|
||||
LangString webview2AbortError ${LANG_KOREAN} "WebView2를 설치하지 못했습니다! WebView2가 없으면 앱을 실행할 수 없습니다. 인스톨러를 다시 시작해보세요."
|
||||
LangString webview2DownloadError ${LANG_KOREAN} "오류: WebView2 다운로드를 실패하였습니다. - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_KOREAN} "WebView2 부트스트래퍼가 성공적으로 다운로드되었습니다."
|
||||
LangString webview2Downloading ${LANG_KOREAN} "WebView2 부트스트래퍼 다운로드 중..."
|
||||
LangString webview2InstallError ${LANG_KOREAN} "오류: 종료 코드 $1로 WebView2를 설치하지 못했습니다."
|
||||
LangString webview2InstallSuccess ${LANG_KOREAN} "WebView2가 성공적으로 설치되었습니다."
|
||||
LangString deleteAppData ${LANG_KOREAN} "애플리케이션 데이터 삭제하기"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_PERSIAN} "اضافه کردن/نصب مجدد کامپونتت"
|
||||
LangString alreadyInstalled ${LANG_PERSIAN} "قبلا نصب شده است"
|
||||
LangString alreadyInstalledLong ${LANG_PERSIAN} "${PRODUCTNAME} ${VERSION} قبلا نصب شده است. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید."
|
||||
LangString appRunning ${LANG_PERSIAN} "{{product_name}} در حال اجر می باشد ! لطفا اول الان را ببندید و دوباره تلاش کنید"
|
||||
LangString appRunningOkKill ${LANG_PERSIAN} "{{product_name}} در حال اجرا می باشد!$\nبرای از بین بردن اوکی را انتخاب کنید"
|
||||
LangString chooseMaintenanceOption ${LANG_PERSIAN} "عملیات نگهداری مدنظر را برای اجرا انتخاب کنید"
|
||||
LangString choowHowToInstall ${LANG_PERSIAN} "نحوه نصب ${PRODUCTNAME} را انتخاب کنید"
|
||||
LangString createDesktop ${LANG_PERSIAN} "ایجاد میانبر دسکتاپ"
|
||||
LangString dontUninstall ${LANG_PERSIAN} "حذف نکنید"
|
||||
LangString dontUninstallDowngrade ${LANG_PERSIAN} "حذف نکنید (تنزل ورژن بدون حذف برای نصب کننده غیرفعال است)"
|
||||
LangString failedToKillApp ${LANG_PERSIAN} "{{product_name}} قابل کشته شدن نیست. اول آن را ببندید و دوباره تلاش کنید"
|
||||
LangString installingWebview2 ${LANG_PERSIAN} "در حال نصب WebView2 ..."
|
||||
LangString newerVersionInstalled ${LANG_PERSIAN} "ورژن جدید ${PRODUCTNAME} قبلا نصب شده است! نصب ورژن قدیمی تر به هیچ عنوان پیشنهاد نمی شود. اگر از این بابت اطمینان دارید , بهتر است ورژن فعلی را حذف کنید. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید."
|
||||
LangString older ${LANG_PERSIAN} "قدیمی تر"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_PERSIAN} "ورژن $R4 ${PRODUCTNAME} قبلا بروی سیستم شما نصب شده است. ر. عملیات مدنظر را انتخاب کنید و بروی بعدی کلیک کنید."
|
||||
LangString silentDowngrades ${LANG_PERSIAN} "تنزل ورژن بدون حذف غیرفعال می باشد, عملیات نصب خاموش غیرقابل انجام است , از رابط گرافیکی برای نصب استفاده کنید.$\n"
|
||||
LangString unableToUninstall ${LANG_PERSIAN} "قابل نصب نیست!"
|
||||
LangString uninstallApp ${LANG_PERSIAN} "حذف ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_PERSIAN} "قبل از نصب , حذف کنید"
|
||||
LangString unknown ${LANG_PERSIAN} "ناشناس"
|
||||
LangString webview2AbortError ${LANG_PERSIAN} "نصب WebView2 شکست خورد! اپ بدون ان کار نمی کند. نصب کننده را دوباره نصب کنید"
|
||||
LangString webview2DownloadError ${LANG_PERSIAN} "ارور: دانلود WebView2 شکست خورد - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_PERSIAN} "WebView2 بوت استرپر با موفقیت نصب شد"
|
||||
LangString webview2Downloading ${LANG_PERSIAN} "دانلود بوت استرپر WebView2..."
|
||||
LangString webview2InstallError ${LANG_PERSIAN} "ارور: نصب WebView2 با کد $1 شکست خورد"
|
||||
LangString webview2InstallSuccess ${LANG_PERSIAN} "WebView2 با موفقیت نصب شد"
|
||||
LangString deleteAppData ${LANG_PERSIAN} "حذف دیتا های اپلیکیشن"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_PORTUGUESE} "Adicionar/Reinstalar componentes"
|
||||
LangString alreadyInstalled ${LANG_PORTUGUESE} "Já instalado"
|
||||
LangString alreadyInstalledLong ${LANG_PORTUGUESE} "${PRODUCTNAME} ${VERSION} já está instalado. Selecione a operação que deseja realizar e clique em Seguinte para continuar."
|
||||
LangString appRunning ${LANG_PORTUGUESE} "{{product_name}} está em execução! Por favor, feche-o primeiro e tente novamente."
|
||||
LangString appRunningOkKill ${LANG_PORTUGUESE} "{{product_name}} está em execução!$\nClique em OK para encerrá-lo."
|
||||
LangString chooseMaintenanceOption ${LANG_PORTUGUESE} "Escolha a opção de manutenção a realizar."
|
||||
LangString choowHowToInstall ${LANG_PORTUGUESE} "Escolha como deseja instalar o ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_PORTUGUESE} "Criar atalho no ambiente de trabalho"
|
||||
LangString dontUninstall ${LANG_PORTUGUESE} "Não desinstalar"
|
||||
LangString dontUninstallDowngrade ${LANG_PORTUGUESE} "Não desinstalar (Instalar uma versão anterior sem desinstalar está desativado neste instalador)"
|
||||
LangString failedToKillApp ${LANG_PORTUGUESE} "Falha ao encerrar {{product_name}}. Por favor, feche-o primeiro e tente novamente."
|
||||
LangString installingWebview2 ${LANG_PORTUGUESE} "A instalar WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_PORTUGUESE} "Uma versão mais recente do ${PRODUCTNAME} já está instalada! Não é recomendada a instalação de uma versão mais antiga. Se realmente deseja instalar esta versão mais antiga, é melhor desinstalar a versão atual primeiro. Selecione a operação que deseja realizar e clique em Seguinte para continuar."
|
||||
LangString older ${LANG_PORTUGUESE} "mais antiga"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_PORTUGUESE} "Uma versão $R4 do ${PRODUCTNAME} está instalada no sistema. Recomenda-se desinstalar a versão atual antes de instalar. Selecione a operação que deseja realizar e clique em Seguinte para continuar."
|
||||
LangString silentDowngrades ${LANG_PORTUGUESE} "Rebaixamentos estão desativados neste instalador, não é possível prosseguir com a instalação silenciosa. Por favor, utilize o instalador com interface gráfica.$\n"
|
||||
LangString unableToUninstall ${LANG_PORTUGUESE} "Não foi possível desinstalar!"
|
||||
LangString uninstallApp ${LANG_PORTUGUESE} "Desinstalar ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_PORTUGUESE} "Desinstalar antes de instalar"
|
||||
LangString unknown ${LANG_PORTUGUESE} "desconhecida"
|
||||
LangString webview2AbortError ${LANG_PORTUGUESE} "Falha ao instalar o WebView2! A aplicação não pode ser executada sem ele. Tente reiniciar o instalador."
|
||||
LangString webview2DownloadError ${LANG_PORTUGUESE} "Erro: Falha ao transferir o WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_PORTUGUESE} "Bootstrapper do WebView2 transferido com sucesso"
|
||||
LangString webview2Downloading ${LANG_PORTUGUESE} "A transferir o Bootstrapper do WebView2..."
|
||||
LangString webview2InstallError ${LANG_PORTUGUESE} "Erro: Instalação do WebView2 falhou com o código $1"
|
||||
LangString webview2InstallSuccess ${LANG_PORTUGUESE} "WebView2 instalado com sucesso"
|
||||
LangString deleteAppData ${LANG_PORTUGUESE} "Eliminar os dados da aplicação"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_PORTUGUESEBR} "Adicionar/Reinstalar componentes"
|
||||
LangString alreadyInstalled ${LANG_PORTUGUESEBR} "Já instalado"
|
||||
LangString alreadyInstalledLong ${LANG_PORTUGUESEBR} "${PRODUCTNAME} ${VERSION} já está instalado. Selecione a operação que deseja realizar e clique Próximo para continuar."
|
||||
LangString appRunning ${LANG_PORTUGUESEBR} "{{product_name}} está aberto! Por favor feche a janela dele e tente novamente."
|
||||
LangString appRunningOkKill ${LANG_PORTUGUESEBR} "{{product_name}} está aberto!$\nClique OK para fechar ele."
|
||||
LangString chooseMaintenanceOption ${LANG_PORTUGUESEBR} "Escolha a opção de manutenção a realizar."
|
||||
LangString choowHowToInstall ${LANG_PORTUGUESEBR} "Escolha como deseja instalar ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_PORTUGUESEBR} "Criar atalho na área de trabalho"
|
||||
LangString dontUninstall ${LANG_PORTUGUESEBR} "Não desinstalar"
|
||||
LangString dontUninstallDowngrade ${LANG_PORTUGUESEBR} "Não desinstalar (Instalar versão anterior sem desinstalar está desabilitado nesse instalador)"
|
||||
LangString failedToKillApp ${LANG_PORTUGUESEBR} "Falha ao fechar {{product_name}}. Por favor feche a janela dele primeiro e tente novamente"
|
||||
LangString installingWebview2 ${LANG_PORTUGUESEBR} "Instalando WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_PORTUGUESEBR} "Uma nova versão do ${PRODUCTNAME} já está instalado! Não é recomendado instalar uma versão anterior. Se realmente deseja instalar essa versão antiga, é recomendado desinstalar a versão atual primeirl. Selecione a operação que deseja executare clique Próximo para continuar."
|
||||
LangString older ${LANG_PORTUGUESEBR} "mais antiga"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_PORTUGUESEBR} "Uma versão $R4 do ${PRODUCTNAME} está instalada no seu sistema. É recomendado desinstalar a versão atual antes de prosseguir com a instalação. Selecione a operação que deseja executare clique Próximo para continuar."
|
||||
LangString silentDowngrades ${LANG_PORTUGUESEBR} "Instalar versão anterior está desabilitado nesse instalador, não foi possível proceder com a instalação silenciosa, por favor use a interface gráfica.$\n"
|
||||
LangString unableToUninstall ${LANG_PORTUGUESEBR} "Não foi possível instalar!"
|
||||
LangString uninstallApp ${LANG_PORTUGUESEBR} "Desinstalar ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_PORTUGUESEBR} "Desinstalar antes de instalar"
|
||||
LangString unknown ${LANG_PORTUGUESEBR} "desconhecida"
|
||||
LangString webview2AbortError ${LANG_PORTUGUESEBR} "Falha ao instalar WebView2! O programa não pode executar sem ele. Tente reiniciar o instalador."
|
||||
LangString webview2DownloadError ${LANG_PORTUGUESEBR} "Erro: Falha ao baixar WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_PORTUGUESEBR} "Bootstrapper do WebView2 baixado com sucesso"
|
||||
LangString webview2Downloading ${LANG_PORTUGUESEBR} "Baixando o Bootstrapper do WebView2..."
|
||||
LangString webview2InstallError ${LANG_PORTUGUESEBR} "Erro: Instalação do Webview2 falhou com código $1"
|
||||
LangString webview2InstallSuccess ${LANG_PORTUGUESEBR} "WebView2 instalado com sucesso"
|
||||
LangString deleteAppData ${LANG_PORTUGUESEBR} "Remover dados do programa"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_RUSSIAN} "Добавить/Переустановить компоненты"
|
||||
LangString alreadyInstalled ${LANG_RUSSIAN} "Уже установлено"
|
||||
LangString alreadyInstalledLong ${LANG_RUSSIAN} "${PRODUCTNAME} ${VERSION} уже установлен. Выберите действие, которое вы хотите выполнить и нажмите Далее для продолжения."
|
||||
LangString appRunning ${LANG_RUSSIAN} "{{product_name}} запущен! Пожалуйста, закройте приложение и попробуйте еще раз."
|
||||
LangString appRunningOkKill ${LANG_RUSSIAN} "{{product_name}} запущен!$\nНажмите OK чтобы закрыть приложение"
|
||||
LangString chooseMaintenanceOption ${LANG_RUSSIAN} "Выберите действие, которое вы хотите выполнить."
|
||||
LangString choowHowToInstall ${LANG_RUSSIAN} "Выберите, как вы хотите установить ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_RUSSIAN} "Добавить ярлык на рабочий стол"
|
||||
LangString dontUninstall ${LANG_RUSSIAN} "Не удалять"
|
||||
LangString dontUninstallDowngrade ${LANG_RUSSIAN} "Не удалять (Установка более ранних версий без удаления невозможна)"
|
||||
LangString failedToKillApp ${LANG_RUSSIAN} "Не удалось закрыть {{product_name}}. Пожалуйста, закройте приложение и попробуйте еще раз"
|
||||
LangString installingWebview2 ${LANG_RUSSIAN} "Установка WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_RUSSIAN} "Более новая версия ${PRODUCTNAME} уже установлена! Не рекомендуется устанавливать более раннюю версию. Если вы действительно хотите установить эту версию, рекомендуется сначала удалить текущую. Выберите действие, которое вы хотите выполнить и нажмите Далее для продолжения."
|
||||
LangString older ${LANG_RUSSIAN} "Более ранняя"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_RUSSIAN} "$R4 версия ${PRODUCTNAME} уже установлена в вашей системе. Рекомендуется удалить текущую версию перед установкой. Выберите действие, которое вы хотите выполнить и нажмите Далее для продолжения."
|
||||
LangString silentDowngrades ${LANG_RUSSIAN} "Установка более ранних версий в фоне невозможна, используйте установщик.$\n"
|
||||
LangString unableToUninstall ${LANG_RUSSIAN} "Не удалось удалить!"
|
||||
LangString uninstallApp ${LANG_RUSSIAN} "Удалить ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_RUSSIAN} "Удалить перед установкой"
|
||||
LangString unknown ${LANG_RUSSIAN} "Неизвестная"
|
||||
LangString webview2AbortError ${LANG_RUSSIAN} "Не удалось установить WebView2! Приложение не может работать без него. Попробуйте перезапустить установщик."
|
||||
LangString webview2DownloadError ${LANG_RUSSIAN} "Ошибка: Не удалось загрузить WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_RUSSIAN} "WebView2 успешно загружен"
|
||||
LangString webview2Downloading ${LANG_RUSSIAN} "Загрузка WebView2..."
|
||||
LangString webview2InstallError ${LANG_RUSSIAN} "Ошибка: Не удалось установить WebView2, код выхода: $1"
|
||||
LangString webview2InstallSuccess ${LANG_RUSSIAN} "WebView2 успешно установлен"
|
||||
LangString deleteAppData ${LANG_RUSSIAN} "Удалить данные приложения"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_SIMPCHINESE} "添加/重新安装组件"
|
||||
LangString alreadyInstalled ${LANG_SIMPCHINESE} "已安装"
|
||||
LangString alreadyInstalledLong ${LANG_SIMPCHINESE} "${PRODUCTNAME} ${VERSION} 已经安装了。选择你想要执行的操作后点击下一步以继续。"
|
||||
LangString appRunning ${LANG_SIMPCHINESE} "{{product_name}} 正在运行!请关闭后再试。"
|
||||
LangString appRunningOkKill ${LANG_SIMPCHINESE} "{{product_name}} 正在运行!$\n点击确定以终止运行。"
|
||||
LangString chooseMaintenanceOption ${LANG_SIMPCHINESE} "选择要执行的维护操作。"
|
||||
LangString choowHowToInstall ${LANG_SIMPCHINESE} "选择你想要安装 ${PRODUCTNAME} 的方式。"
|
||||
LangString createDesktop ${LANG_SIMPCHINESE} "创建桌面快捷方式"
|
||||
LangString dontUninstall ${LANG_SIMPCHINESE} "请勿卸载"
|
||||
LangString dontUninstallDowngrade ${LANG_SIMPCHINESE} "请勿卸载(此安装程序禁止未卸载就进行版本降级的操作)"
|
||||
LangString failedToKillApp ${LANG_SIMPCHINESE} "无法终止 {{product_name}}。请关闭后再试。"
|
||||
LangString installingWebview2 ${LANG_SIMPCHINESE} "正在安装 WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_SIMPCHINESE} "有一个更新版本的 ${PRODUCTNAME} 已经安装了!不推荐你安装旧的版本。如果你真的想要安装这个旧的版本,推荐先卸载当前版本。选择你想要执行的操作后点击下一步以继续。"
|
||||
LangString older ${LANG_SIMPCHINESE} "旧的"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_SIMPCHINESE} "系统中已存在版本为 $R4 的 ${PRODUCTNAME}。推荐先卸载当前版本后再进行安装。选择你想要执行的操作后点击下一步以继续。"
|
||||
LangString silentDowngrades ${LANG_SIMPCHINESE} "降级操作在此安装程序上已禁用,无法进行安静安装,请使用图形操作界面。$\n"
|
||||
LangString unableToUninstall ${LANG_SIMPCHINESE} "无法卸载!"
|
||||
LangString uninstallApp ${LANG_SIMPCHINESE} "卸载 ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_SIMPCHINESE} "安装前卸载"
|
||||
LangString unknown ${LANG_SIMPCHINESE} "未知"
|
||||
LangString webview2AbortError ${LANG_SIMPCHINESE} "无法安装 WebView2!没有它,此应用就无法运行。尝试重启安装程序。"
|
||||
LangString webview2DownloadError ${LANG_SIMPCHINESE} "错误:无法下载 WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_SIMPCHINESE} "WebView2 引导程序下载成功"
|
||||
LangString webview2Downloading ${LANG_SIMPCHINESE} "正在下载 WebView2 引导程序..."
|
||||
LangString webview2InstallError ${LANG_SIMPCHINESE} "错误:安装 WebView2 时失败,错误代码:$1"
|
||||
LangString webview2InstallSuccess ${LANG_SIMPCHINESE} "成功安装 WebView2"
|
||||
LangString deleteAppData ${LANG_SIMPCHINESE} "删除应用程序数据"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_SPANISH} "Añadir o reinstalar componentes"
|
||||
LangString alreadyInstalled ${LANG_SPANISH} "Ya está instalado"
|
||||
LangString alreadyInstalledLong ${LANG_SPANISH} "${PRODUCTNAME} ${VERSION} ya está instalado. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString appRunning ${LANG_SPANISH} "¡{{product_name}} está abierto! Por favor ciérrelo e intente de nuevo."
|
||||
LangString appRunningOkKill ${LANG_SPANISH} "¡{{product_name}} está abierto!$\nPulse Aceptar para cerrarlo."
|
||||
LangString chooseMaintenanceOption ${LANG_SPANISH} "Elija la operación de mantenimiento que desee realizar."
|
||||
LangString choowHowToInstall ${LANG_SPANISH} "Elija cómo desea instalar ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_SPANISH} "Crear acceso directo en el escritorio"
|
||||
LangString dontUninstall ${LANG_SPANISH} "No desinstalar"
|
||||
LangString dontUninstallDowngrade ${LANG_SPANISH} "No desinstalar (Disminuir la versión sin desinstalar está deshabilitado para este instalador)"
|
||||
LangString failedToKillApp ${LANG_SPANISH} "No se ha podido cerrar {{product_name}}. Por favor ciérrelo e intente de nuevo."
|
||||
LangString installingWebview2 ${LANG_SPANISH} "Instalando WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_SPANISH} "Ya está instalada una versión más reciente de ${PRODUCTNAME}. No se recomienda que instale una versión anterior. Si realmente desea instalar esta versión anterior, es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString older ${LANG_SPANISH} "anterior"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_SPANISH} "Una versión $R4 de ${PRODUCTNAME} está instalada en su sistema. Es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString silentDowngrades ${LANG_SPANISH} "Disminuir la versión está deshabilitado para este instalador. No se puede continuar con el instalador silencioso, por favor use el instalador de interfaz gráfica.$\n"
|
||||
LangString unableToUninstall ${LANG_SPANISH} "No se ha podido desinstalar."
|
||||
LangString uninstallApp ${LANG_SPANISH} "Desinstalar ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_SPANISH} "Desinstalar antes de instalar"
|
||||
LangString unknown ${LANG_SPANISH} "desconocida"
|
||||
LangString webview2AbortError ${LANG_SPANISH} "No se ha podido instalar WebView2. Intente reiniciar el instalador."
|
||||
LangString webview2DownloadError ${LANG_SPANISH} "Error: No se ha podido descargar WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_SPANISH} "El bootstrapper de WebView2 fue descargado con éxito."
|
||||
LangString webview2Downloading ${LANG_SPANISH} "Descargando el bootstrapper de WebView2..."
|
||||
LangString webview2InstallError ${LANG_SPANISH} "Error: La instalación de WebView2 falló con el código $1."
|
||||
LangString webview2InstallSuccess ${LANG_SPANISH} "WebView2 fue instalado con éxito."
|
||||
LangString deleteAppData ${LANG_SPANISH} "Eliminar los datos de aplicación"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_SPANISHINTERNATIONAL} "Añadir o reinstalar componentes"
|
||||
LangString alreadyInstalled ${LANG_SPANISHINTERNATIONAL} "Ya está instalado"
|
||||
LangString alreadyInstalledLong ${LANG_SPANISHINTERNATIONAL} "${PRODUCTNAME} ${VERSION} ya está instalado. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString appRunning ${LANG_SPANISHINTERNATIONAL} "¡{{product_name}} está abierto! Por favor ciérrelo e intente de nuevo."
|
||||
LangString appRunningOkKill ${LANG_SPANISHINTERNATIONAL} "¡{{product_name}} está abierto!$\nPulse Aceptar para cerrarlo."
|
||||
LangString chooseMaintenanceOption ${LANG_SPANISHINTERNATIONAL} "Elija la operación de mantenimiento que desee realizar."
|
||||
LangString choowHowToInstall ${LANG_SPANISHINTERNATIONAL} "Elija cómo desea instalar ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_SPANISHINTERNATIONAL} "Crear acceso directo en el escritorio"
|
||||
LangString dontUninstall ${LANG_SPANISHINTERNATIONAL} "No desinstalar"
|
||||
LangString dontUninstallDowngrade ${LANG_SPANISHINTERNATIONAL} "No desinstalar (Disminuir la versión sin desinstalar está deshabilitado para este instalador)"
|
||||
LangString failedToKillApp ${LANG_SPANISHINTERNATIONAL} "No se ha podido cerrar {{product_name}}. Por favor ciérrelo e intente de nuevo."
|
||||
LangString installingWebview2 ${LANG_SPANISHINTERNATIONAL} "Instalando WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_SPANISHINTERNATIONAL} "Ya está instalada una versión más reciente de ${PRODUCTNAME}. No se recomienda que instale una versión anterior. Si realmente desea instalar esta versión anterior, es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString older ${LANG_SPANISHINTERNATIONAL} "anterior"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_SPANISHINTERNATIONAL} "Una versión $R4 de ${PRODUCTNAME} está instalada en su sistema. Es recomendable desinstalar la versión actual antes de continuar. Seleccione la operación que desee realizar y pulse Siguiente para continuar."
|
||||
LangString silentDowngrades ${LANG_SPANISHINTERNATIONAL} "Disminuir la versión está deshabilitado para este instalador. No se puede continuar con el instalador silencioso, por favor use el instalador de interfaz gráfica.$\n"
|
||||
LangString unableToUninstall ${LANG_SPANISHINTERNATIONAL} "No se ha podido desinstalar."
|
||||
LangString uninstallApp ${LANG_SPANISHINTERNATIONAL} "Desinstalar ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_SPANISHINTERNATIONAL} "Desinstalar antes de instalar"
|
||||
LangString unknown ${LANG_SPANISHINTERNATIONAL} "desconocida"
|
||||
LangString webview2AbortError ${LANG_SPANISHINTERNATIONAL} "No se ha podido instalar WebView2. Intente reiniciar el instalador."
|
||||
LangString webview2DownloadError ${LANG_SPANISHINTERNATIONAL} "Error: No se ha podido descargar WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_SPANISHINTERNATIONAL} "El bootstrapper de WebView2 fue descargado con éxito."
|
||||
LangString webview2Downloading ${LANG_SPANISHINTERNATIONAL} "Descargando el bootstrapper de WebView2..."
|
||||
LangString webview2InstallError ${LANG_SPANISHINTERNATIONAL} "Error: La instalación de WebView2 falló con el código $1."
|
||||
LangString webview2InstallSuccess ${LANG_SPANISHINTERNATIONAL} "WebView2 fue instalado con éxito."
|
||||
LangString deleteAppData ${LANG_SPANISHINTERNATIONAL} "Eliminar los datos de aplicación"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_SWEDISH} "Lägg till/Installera om komponenter"
|
||||
LangString alreadyInstalled ${LANG_SWEDISH}} "Redan installerad"
|
||||
LangString alreadyInstalledLong ${LANG_SWEDISH}} "${PRODUCTNAME} ${VERSION} är redan installerad. Välj åtgärd och klicka på Nästa för att fortsätta."
|
||||
LangString appRunning ${LANG_SWEDISH} "{{product_name}} körs! Stäng det först och försök igen."
|
||||
LangString appRunningOkKill ${LANG_SWEDISH} "{{product_name}} körs!$\nKlicka på OK för att avsluta det."
|
||||
LangString chooseMaintenanceOption ${LANG_SWEDISH} "Välj underhållsåtgärd."
|
||||
LangString choowHowToInstall ${LANG_SWEDISH} "Välj hur du vill installera ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_SWEDISH} "Skapa genväg på skrivbordet"
|
||||
LangString dontUninstall ${LANG_SWEDISH} "Avinstallera inte"
|
||||
LangString dontUninstallDowngrade ${LANG_SWEDISH} "Avinstallera inte (nedgradering utan avinstallation är inaktiverad för den här installationsprogrammet)"
|
||||
LangString failedToKillApp ${LANG_SWEDISH} "Kunde inte avsluta {{product_name}}. Stäng det först och försök igen."
|
||||
LangString installingWebview2 ${LANG_SWEDISH} "Installerar WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_SWEDISH} "En nyare version av ${PRODUCTNAME} är redan installerad! Det rekommenderas inte att installera en äldre version. Om du verkligen vill installera denna äldre version är det bättre att avinstallera den nuvarande versionen först. Välj åtgärd och klicka på Nästa för att fortsätta."
|
||||
LangString older ${LANG_SWEDISH} "äldre"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_SWEDISH} "En $R4-version av ${PRODUCTNAME} är installerad på ditt system. Det rekommenderas att du avinstallerar den nuvarande versionen innan du installerar. Välj åtgärd och klicka på Nästa för att fortsätta."
|
||||
LangString silentDowngrades ${LANG_SWEDISH} "Nedgraderingar är inaktiverade för den här installationsprogrammet. Kan inte fortsätta med installationsprogrammet. Använd det grafiska installationsprogrammet istället.$\n"
|
||||
LangString unableToUninstall ${LANG_SWEDISH} "Kan inte avinstallera!"
|
||||
LangString uninstallApp ${LANG_SWEDISH} "Avinstallera ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_SWEDISH} "Avinstallera innan installation"
|
||||
LangString unknown ${LANG_SWEDISH} "okänd"
|
||||
LangString webview2AbortError ${LANG_SWEDISH} "Misslyckades med att installera WebView2! Appen kan inte köras utan det. Försök starta om installationsprogrammet."
|
||||
LangString webview2DownloadError ${LANG_SWEDISH} "Fel: Nedladdning av WebView2 misslyckades - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_SWEDISH} "WebView2 bootstrapper nedladdad framgångsrikt"
|
||||
LangString webview2Downloading ${LANG_SWEDISH} "Laddar ner WebView2 bootstrapper..."
|
||||
LangString webview2InstallError ${LANG_SWEDISH} "Fel: Installation av WebView2 misslyckades med felkod $1"
|
||||
LangString webview2InstallSuccess ${LANG_SWEDISH} "WebView2 installerades framgångsrikt"
|
||||
LangString deleteAppData ${LANG_SWEDISH} "Ta bort applikationsdata"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_TRADCHINESE} "增加或重新安裝元件"
|
||||
LangString alreadyInstalled ${LANG_TRADCHINESE} "已安裝"
|
||||
LangString alreadyInstalledLong ${LANG_TRADCHINESE} "${PRODUCTNAME} ${VERSION} 已經安裝了。選擇你想要進行的操作並且點選下一步。"
|
||||
LangString appRunning ${LANG_TRADCHINESE} "{{product_name}} 正在執行中!請先關閉再進行嘗試。"
|
||||
LangString appRunningOkKill ${LANG_TRADCHINESE} "{{product_name}} 正在執行中!點選確定後終止。"
|
||||
LangString chooseMaintenanceOption ${LANG_TRADCHINESE} "請選擇你要進行的維護選項。"
|
||||
LangString choowHowToInstall ${LANG_TRADCHINESE} "選擇你要如何安裝 ${PRODUCTNAME}。"
|
||||
LangString createDesktop ${LANG_TRADCHINESE} "建立桌面捷徑"
|
||||
LangString dontUninstall ${LANG_TRADCHINESE} "請勿解除安裝"
|
||||
LangString dontUninstallDowngrade ${LANG_TRADCHINESE} "請勿解除安裝(本安裝程式不允許未解除安裝就進行版本降低的操作)"
|
||||
LangString failedToKillApp ${LANG_TRADCHINESE} "無法終止 {{product_name}}。請先關閉再進行嘗試。"
|
||||
LangString installingWebview2 ${LANG_TRADCHINESE} "WebView2 安裝中..."
|
||||
LangString newerVersionInstalled ${LANG_TRADCHINESE} "已安裝更新版本的 ${PRODUCTNAME}!不建議安裝舊版。如果真的想要安裝舊版的話,最好先解除安裝現在的版本。選擇你想要進行的操作後再進行下一步。"
|
||||
LangString older ${LANG_TRADCHINESE} "舊版"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_TRADCHINESE} "你的系統已經安裝 ${PRODUCTNAME} 的 $R4 版本。建議你先解除安裝現在的版本後再進行安裝。選擇你想要進行的操作後再進行下一步。"
|
||||
LangString silentDowngrades ${LANG_TRADCHINESE} "本安裝程式不允許未解除安裝就進行版本降低的操作,無法繼續無聲安裝,請改用圖形化介面安裝。$\n"
|
||||
LangString unableToUninstall ${LANG_TRADCHINESE} "無法解除安裝!"
|
||||
LangString uninstallApp ${LANG_TRADCHINESE} "解除安裝 ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_TRADCHINESE} "安裝前先解除安裝"
|
||||
LangString unknown ${LANG_TRADCHINESE} "未知"
|
||||
LangString webview2AbortError ${LANG_TRADCHINESE} "無法安裝 WebView2!這個應用程式需要安裝 WebView2 才能執行。請重新啟動安裝程式。"
|
||||
LangString webview2DownloadError ${LANG_TRADCHINESE} "錯誤:WebView2 下載失敗 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_TRADCHINESE} "WebView2 啟動載入器下載成功"
|
||||
LangString webview2Downloading ${LANG_TRADCHINESE} "正在下載 WebView2 啟動載入器..."
|
||||
LangString webview2InstallError ${LANG_TRADCHINESE} "錯誤:WebView2 安裝失敗,錯誤碼 $1"
|
||||
LangString webview2InstallSuccess ${LANG_TRADCHINESE} "WebView2 安裝成功"
|
||||
LangString deleteAppData ${LANG_TRADCHINESE} "刪除應用程式數據"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_TURKISH} "Bileşen Ekle/Yeniden Yükle"
|
||||
LangString alreadyInstalled ${LANG_TURKISH} "Daha Önceden Yüklenmiş"
|
||||
LangString alreadyInstalledLong ${LANG_TURKISH} "${PRODUCTNAME} ${VERSION} daha önceden yüklenmiş. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın."
|
||||
LangString appRunning ${LANG_TURKISH} "{{product_name}} çalışır durumda! Lütfen önce uygulamayı kapatın ve sonra tekrar deneyin."
|
||||
LangString appRunningOkKill ${LANG_TURKISH} "{{product_name}} çalışır durumda!$\nUygulamayı sonlandırmak için Tamam'a tıklayın."
|
||||
LangString chooseMaintenanceOption ${LANG_TURKISH} "Gerçekleştirmek istediğiniz bakım seçeneğini belirleyin."
|
||||
LangString choowHowToInstall ${LANG_TURKISH} "${PRODUCTNAME} uygulamasını nasıl yüklemek istediğinizi seçin."
|
||||
LangString createDesktop ${LANG_TURKISH} "Masaüstü kısayolu oluştur"
|
||||
LangString dontUninstall ${LANG_TURKISH} "Kaldırma işlemini gerçekleştirme"
|
||||
LangString dontUninstallDowngrade ${LANG_TURKISH} "Kaldırma işlemini gerçekleştirme (Kaldırma işlemi yapmadan sürüm düşürme bu yükleyici için devre dışı bırakılmıştır)"
|
||||
LangString failedToKillApp ${LANG_TURKISH} "{{product_name}} sonlandırılamadı. Lütfen önce kapatın sonra tekrar deneyin."
|
||||
LangString installingWebview2 ${LANG_TURKISH} "WebView2 yükleniyor..."
|
||||
LangString newerVersionInstalled ${LANG_TURKISH} "${PRODUCTNAME} uygulamasının daha yeni bir sürümü zaten yüklü! Daha eski bir sürümü yüklemeniz önerilmez. Bu eski sürümü gerçekten yüklemek istiyorsanız, önce mevcut sürümü kaldırmanız daha uygundur. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın."
|
||||
LangString older ${LANG_TURKISH} "daha eski"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_TURKISH} "Sisteminizde ${PRODUCTNAME} uygulamasının $R4 sürümü yüklü. Yükleme işleminden önce mevcut sürümü kaldırmanız önerilir. Gerçekleştirmek istediğiniz işlemi seçin ve devam etmek için İleri'ye tıklayın."
|
||||
LangString silentDowngrades ${LANG_TURKISH} "Bu yükleyici için sürüm düşürme işlemleri devre dışı bırakıldı, sessiz yükleyici ile devam edilemiyor, lütfen bunun yerine grafik arayüz yükleyicisini kullanın.$\n"
|
||||
LangString unableToUninstall ${LANG_TURKISH} "Kaldırma işlemi gerçekleştirilemiyor!"
|
||||
LangString uninstallApp ${LANG_TURKISH} "${PRODUCTNAME}'i kaldır"
|
||||
LangString uninstallBeforeInstalling ${LANG_TURKISH} "Yükleme yapmadan önce kaldırın"
|
||||
LangString unknown ${LANG_TURKISH} "bilinmeyen"
|
||||
LangString webview2AbortError ${LANG_TURKISH} "WebView2 yüklenemedi! Uygulama bu bileşen olmadan çalışamaz. Yükleyiciyi yeniden başlatmayı deneyin."
|
||||
LangString webview2DownloadError ${LANG_TURKISH} "Hata: WebView2 İndirmesi Başarısız - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_TURKISH} "WebView2 önyükleyicisi başarıyla indirildi"
|
||||
LangString webview2Downloading ${LANG_TURKISH} "WebView2 önyükleyicisi indiriliyor..."
|
||||
LangString webview2InstallError ${LANG_TURKISH} "Hata: WebView2 yüklemesi $1 hata koduyla başarısız oldu."
|
||||
LangString webview2InstallSuccess ${LANG_TURKISH} "WebView2 başarıyla yüklendi"
|
||||
LangString deleteAppData ${LANG_TURKISH} "Uygulama verilerini sil"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_UKRAINIAN} "Додати/Перевстановити компоненти"
|
||||
LangString alreadyInstalled ${LANG_UKRAINIAN} "Вже встановлено"
|
||||
LangString alreadyInstalledLong ${LANG_UKRAINIAN} "${PRODUCTNAME} ${VERSION} вже встановлено. Виберіть дію, яку ви хочете виконати, і натисніть Далі, щоб продовжити."
|
||||
LangString appRunning ${LANG_UKRAINIAN} "{{product_name}} запущено! Будь ласка, спочатку закрийте його, а потім спробуйте ще раз."
|
||||
LangString appRunningOkKill ${LANG_UKRAINIAN} "{{product_name}} запущено!$\nНатисніть ОК, щоб примусово закрити його"
|
||||
LangString chooseMaintenanceOption ${LANG_UKRAINIAN} "Виберіть дію, яку треба виконати."
|
||||
LangString choowHowToInstall ${LANG_UKRAINIAN} "Виберіть, як ви хочете встановити ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_UKRAINIAN} "Створити ярлик на робочому столі"
|
||||
LangString dontUninstall ${LANG_UKRAINIAN} "Не видаляти"
|
||||
LangString dontUninstallDowngrade ${LANG_UKRAINIAN} "Не видаляти (для цього встановлювача вимкнено зниження версії без видалення)"
|
||||
LangString failedToKillApp ${LANG_UKRAINIAN} "Не вдалося примусово закрити {{product_name}}. Будь ласка, спочатку закрийте його, а потім спробуйте ще раз"
|
||||
LangString installingWebview2 ${LANG_UKRAINIAN} "Встановлення WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_UKRAINIAN} "Новіша версія ${PRODUCTNAME} вже встановлена! Встановлювати старішу версію не рекомендується. Якщо ви дійсно хочете встановити цю версію, краще спочатку видаліть поточну. Виберіть дію, яку ви хочете виконати, і натисніть Далі, щоб продовжити."
|
||||
LangString older ${LANG_UKRAINIAN} "старішу"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_UKRAINIAN} "У вашій системі вже встановлено $R4 версію ${PRODUCTNAME}. Рекомендується видалити поточну версію перед встановленням. Виберіть дію, яку ви хочете виконати, і натисніть Далі, щоб продовжити."
|
||||
LangString silentDowngrades ${LANG_UKRAINIAN} "Для цього встановлювача вимкнено зниження версій. Неможливо продовжити роботу з фоновим встановлювачем. Будь ласка, скористайтеся встановлювачем з графічним інтерфейсом.$\n"
|
||||
LangString unableToUninstall ${LANG_UKRAINIAN} "Не вдалося видалити!"
|
||||
LangString uninstallApp ${LANG_UKRAINIAN} "Видалити ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_UKRAINIAN} "Видалити перед встановленням"
|
||||
LangString unknown ${LANG_UKRAINIAN} "невідому"
|
||||
LangString webview2AbortError ${LANG_UKRAINIAN} "Не вдалося встановити WebView2! Без нього програма не може працювати. Спробуйте перезапустити встановлювач."
|
||||
LangString webview2DownloadError ${LANG_UKRAINIAN} "Помилка: не вдалося завантажити WebView2 - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_UKRAINIAN} "WebView2 успішно завантажено"
|
||||
LangString webview2Downloading ${LANG_UKRAINIAN} "Завантаження WebView2..."
|
||||
LangString webview2InstallError ${LANG_UKRAINIAN} "Помилка: не вдалося встановити WebView2, код виходу - $1"
|
||||
LangString webview2InstallSuccess ${LANG_UKRAINIAN} "WebView2 успішно встановлено "
|
||||
LangString deleteAppData ${LANG_UKRAINIAN} "Видалити дані програми"
|
||||
@@ -0,0 +1,27 @@
|
||||
LangString addOrReinstall ${LANG_VIETNAMESE} "Thêm/Cài đặt lại các thành phần"
|
||||
LangString alreadyInstalled ${LANG_VIETNAMESE} "Đã được cài đặt"
|
||||
LangString alreadyInstalledLong ${LANG_VIETNAMESE} "${PRODUCTNAME} ${VERSION} đã được cài đặt. Hãy chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục."
|
||||
LangString appRunning ${LANG_VIETNAMESE} "{{product_name}} đang chạy! Vui lòng đóng ứng dụng trước rồi thử lại."
|
||||
LangString appRunningOkKill ${LANG_VIETNAMESE} "{{product_name}} đang chạy!$\nNhấn OK để tắt ứng dụng"
|
||||
LangString chooseMaintenanceOption ${LANG_VIETNAMESE} "Chọn thao tác bảo trì bạn muốn thực hiện."
|
||||
LangString choowHowToInstall ${LANG_VIETNAMESE} "Chọn cách bạn muốn cài đặt ${PRODUCTNAME}."
|
||||
LangString createDesktop ${LANG_VIETNAMESE} "Tạo biểu tượng ngoài màn hình"
|
||||
LangString dontUninstall ${LANG_VIETNAMESE} "Không gỡ cài đặt"
|
||||
LangString dontUninstallDowngrade ${LANG_VIETNAMESE} "Không gỡ cài đặt (Không hỗ trợ hạ cấp mà không gỡ cài đặt trong bộ cài này)"
|
||||
LangString failedToKillApp ${LANG_VIETNAMESE} "Không thể tắt {{product_name}}. Vui lòng đóng ứng dụng trước rồi thử lại"
|
||||
LangString installingWebview2 ${LANG_VIETNAMESE} "Đang cài đặt WebView2..."
|
||||
LangString newerVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản mới hơn của ${PRODUCTNAME} đã được cài đặt! Không khuyến nghị cài đặt phiên bản cũ hơn. Nếu bạn vẫn muốn cài phiên bản này, hãy gỡ phiên bản hiện tại trước. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục."
|
||||
LangString older ${LANG_VIETNAMESE} "cũ hơn"
|
||||
LangString olderOrUnknownVersionInstalled ${LANG_VIETNAMESE} "Một phiên bản $R4 của ${PRODUCTNAME} đã được cài trên hệ thống. Khuyến nghị gỡ phiên bản hiện tại trước khi cài đặt. Chọn thao tác bạn muốn thực hiện và nhấn Next để tiếp tục."
|
||||
LangString silentDowngrades ${LANG_VIETNAMESE} "Không hỗ trợ hạ cấp trong chế độ cài đặt im lặng, không thể tiếp tục. Vui lòng sử dụng trình cài đặt giao diện đồ họa.$\n"
|
||||
LangString unableToUninstall ${LANG_VIETNAMESE} "Không thể gỡ cài đặt!"
|
||||
LangString uninstallApp ${LANG_VIETNAMESE} "Gỡ cài đặt ${PRODUCTNAME}"
|
||||
LangString uninstallBeforeInstalling ${LANG_VIETNAMESE} "Gỡ cài đặt trước khi cài đặt"
|
||||
LangString unknown ${LANG_VIETNAMESE} "không xác định"
|
||||
LangString webview2AbortError ${LANG_VIETNAMESE} "Cài đặt WebView2 thất bại! Ứng dụng không thể chạy nếu thiếu thành phần này. Hãy thử khởi động lại trình cài đặt."
|
||||
LangString webview2DownloadError ${LANG_VIETNAMESE} "Lỗi: Tải WebView2 thất bại - $0"
|
||||
LangString webview2DownloadSuccess ${LANG_VIETNAMESE} "Tải trình cài đặt WebView2 thành công"
|
||||
LangString webview2Downloading ${LANG_VIETNAMESE} "Đang tải trình cài đặt WebView2..."
|
||||
LangString webview2InstallError ${LANG_VIETNAMESE} "Lỗi: Cài đặt WebView2 thất bại với mã lỗi $1"
|
||||
LangString webview2InstallSuccess ${LANG_VIETNAMESE} "Cài đặt WebView2 thành công"
|
||||
LangString deleteAppData ${LANG_VIETNAMESE} "Xóa dữ liệu ứng dụng"
|
||||
952
crates/tauri-bundler/src/bundle/windows/nsis/mod.rs
Normal file
952
crates/tauri-bundler/src/bundle/windows/nsis/mod.rs
Normal file
@@ -0,0 +1,952 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use crate::{
|
||||
Error, Settings,
|
||||
bundle::{
|
||||
settings::Arch,
|
||||
windows::{
|
||||
sign::{should_sign, sign_command, try_sign},
|
||||
util::{
|
||||
NSIS_OUTPUT_FOLDER_NAME, NSIS_UPDATER_OUTPUT_FOLDER_NAME, download_webview2_bootstrapper,
|
||||
download_webview2_offline_installer, vc_runtime_dlls,
|
||||
},
|
||||
},
|
||||
},
|
||||
error::{ErrorExt, bail},
|
||||
utils::{
|
||||
CommandExt,
|
||||
http_utils::{HashAlgorithm, download_and_verify, verify_file_hash},
|
||||
},
|
||||
};
|
||||
use tauri_utils::display_path;
|
||||
|
||||
use crate::error::Context;
|
||||
use handlebars::{Handlebars, to_json};
|
||||
use tauri_utils::config::{NSISInstallerMode, NsisCompression, WebviewInstallMode};
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
// URLS for the NSIS toolchain.
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_URL: &str =
|
||||
"https://github.com/tauri-apps/binary-releases/releases/download/nsis-3.11/nsis-3.11.zip";
|
||||
#[cfg(target_os = "windows")]
|
||||
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.3/nsis_tauri_utils.dll";
|
||||
const NSIS_TAURI_UTILS_SHA1: &str = "75197FEE3C6A814FE035788D1C34EAD39349B860";
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const NSIS_REQUIRED_FILES: &[&str] = &[
|
||||
"makensis.exe",
|
||||
"Bin/makensis.exe",
|
||||
"Stubs/lzma-x86-unicode",
|
||||
"Stubs/lzma_solid-x86-unicode",
|
||||
"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",
|
||||
"StartMenu.dll",
|
||||
"System.dll",
|
||||
"nsDialogs.dll",
|
||||
"additional/nsis_tauri_utils.dll",
|
||||
];
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
const NSIS_REQUIRED_FILES: &[&str] = &["Plugins/x86-unicode/additional/nsis_tauri_utils.dll"];
|
||||
|
||||
const NSIS_REQUIRED_FILES_HASH: &[(&str, &str, &str, HashAlgorithm)] = &[(
|
||||
"Plugins/x86-unicode/additional/nsis_tauri_utils.dll",
|
||||
NSIS_TAURI_UTILS_URL,
|
||||
NSIS_TAURI_UTILS_SHA1,
|
||||
HashAlgorithm::Sha1,
|
||||
)];
|
||||
|
||||
/// Runs all of the commands to build the NSIS installer.
|
||||
/// Returns a vector of PathBuf that shows where the NSIS installer was created.
|
||||
pub fn bundle_project(settings: &Settings, updater: bool) -> crate::Result<Vec<PathBuf>> {
|
||||
let tauri_tools_path = settings
|
||||
.local_tools_directory()
|
||||
.map(|d| d.join(".tauri"))
|
||||
.unwrap_or_else(|| dirs::cache_dir().unwrap().join("tauri"));
|
||||
|
||||
let nsis_toolset_path = tauri_tools_path.join("NSIS");
|
||||
|
||||
if !nsis_toolset_path.exists() {
|
||||
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
|
||||
} else if NSIS_REQUIRED_FILES
|
||||
.iter()
|
||||
.any(|p| !nsis_toolset_path.join(p).exists())
|
||||
{
|
||||
log::warn!("NSIS directory is missing some files. Recreating it.");
|
||||
std::fs::remove_dir_all(&nsis_toolset_path)?;
|
||||
get_and_extract_nsis(&nsis_toolset_path, &tauri_tools_path)?;
|
||||
} else {
|
||||
let mismatched = NSIS_REQUIRED_FILES_HASH
|
||||
.iter()
|
||||
.filter(|(p, _, hash, hash_algorithm)| {
|
||||
verify_file_hash(nsis_toolset_path.join(p), hash, *hash_algorithm).is_err()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !mismatched.is_empty() {
|
||||
log::warn!("NSIS directory contains mis-hashed files. Redownloading them.");
|
||||
for (path, url, hash, hash_algorithm) in mismatched {
|
||||
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")?)
|
||||
.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())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build_nsis_app_installer(settings, &nsis_toolset_path, &tauri_tools_path, updater)
|
||||
}
|
||||
|
||||
// Gets NSIS and verifies the download via Sha1
|
||||
fn get_and_extract_nsis(nsis_toolset_path: &Path, _tauri_tools_path: &Path) -> crate::Result<()> {
|
||||
log::info!("Verifying NSIS package");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let data = download_and_verify(NSIS_URL, NSIS_SHA1, HashAlgorithm::Sha1)?;
|
||||
log::info!("extracting NSIS");
|
||||
crate::utils::http_utils::extract_zip(&data, _tauri_tools_path)?;
|
||||
fs::rename(_tauri_tools_path.join("nsis-3.11"), nsis_toolset_path)?;
|
||||
}
|
||||
|
||||
// download additional plugins
|
||||
let nsis_plugins = nsis_toolset_path.join("Plugins");
|
||||
|
||||
let data = download_and_verify(
|
||||
NSIS_TAURI_UTILS_URL,
|
||||
NSIS_TAURI_UTILS_SHA1,
|
||||
HashAlgorithm::Sha1,
|
||||
)?;
|
||||
|
||||
let target_folder = nsis_plugins.join("x86-unicode").join("additional");
|
||||
fs::create_dir_all(&target_folder)?;
|
||||
fs::write(target_folder.join("nsis_tauri_utils.dll"), data)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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() {
|
||||
return Ok(format!(
|
||||
"{}.{}.{}.{}",
|
||||
version.major, version.minor, version.patch, version.build
|
||||
));
|
||||
} else {
|
||||
log::warn!(
|
||||
"Unable to parse version build metadata. Numeric value expected, received: `{}`. This will be replaced with `0` in `VIProductVersion` because Windows requires this field to be numeric.",
|
||||
version.build
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(format!(
|
||||
"{}.{}.{}.0",
|
||||
version.major, version.minor, version.patch,
|
||||
))
|
||||
}
|
||||
|
||||
fn build_nsis_app_installer(
|
||||
settings: &Settings,
|
||||
#[allow(unused_variables)] nsis_toolset_path: &Path,
|
||||
tauri_tools_path: &Path,
|
||||
updater: bool,
|
||||
) -> crate::Result<Vec<PathBuf>> {
|
||||
let arch = match settings.binary_arch() {
|
||||
Arch::X86_64 => "x64",
|
||||
Arch::X86 => "x86",
|
||||
Arch::AArch64 => "arm64",
|
||||
target => {
|
||||
return Err(crate::Error::ArchError(format!(
|
||||
"unsupported architecture: {target:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
log::info!("Target: {}", arch);
|
||||
|
||||
let output_path = settings.project_out_directory().join("nsis").join(arch);
|
||||
if output_path.exists() {
|
||||
fs::remove_dir_all(&output_path)?;
|
||||
}
|
||||
fs::create_dir_all(&output_path)?;
|
||||
|
||||
// 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.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)
|
||||
.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();
|
||||
|
||||
let plugins_path = output_path.join("Plugins");
|
||||
// copy system plugins (we don't want to modify system installed DLLs, and on some systems there will even be permission errors if we try)
|
||||
crate::utils::fs_utils::copy_dir(
|
||||
&system_nsis_toolset_path.join("Plugins").join("x86-unicode"),
|
||||
&plugins_path.join("x86-unicode"),
|
||||
)
|
||||
.context("failed to copy system NSIS Plugins folder to local copy")?;
|
||||
// copy our downloaded DLLs
|
||||
crate::utils::fs_utils::copy_dir(
|
||||
&nsis_toolset_path
|
||||
.join("Plugins")
|
||||
.join("x86-unicode")
|
||||
.join("additional"),
|
||||
&plugins_path.join("x86-unicode").join("additional"),
|
||||
)
|
||||
.context("failed to copy additional NSIS Plugins folder to local copy")?;
|
||||
Some(plugins_path)
|
||||
} else {
|
||||
// in this case plugin_copy_path can be None, we'll use the system default path
|
||||
None
|
||||
};
|
||||
|
||||
let mut data = BTreeMap::new();
|
||||
|
||||
let bundle_id = settings.bundle_identifier();
|
||||
let manufacturer = settings
|
||||
.publisher()
|
||||
.unwrap_or_else(|| bundle_id.split('.').nth(1).unwrap_or(bundle_id));
|
||||
|
||||
let additional_plugins_path = maybe_plugin_copy_path
|
||||
.clone()
|
||||
.unwrap_or_else(|| nsis_toolset_path.join("Plugins"))
|
||||
.join("x86-unicode")
|
||||
.join("additional");
|
||||
|
||||
data.insert(
|
||||
"additional_plugins_path",
|
||||
// either our Plugins copy (when signing) or the cache/Plugins/x86-unicode path
|
||||
to_json(&additional_plugins_path),
|
||||
);
|
||||
|
||||
if let Some(plugin_copy_path) = &maybe_plugin_copy_path {
|
||||
data.insert(
|
||||
"signed_plugins_path",
|
||||
to_json(plugin_copy_path.join("x86-unicode")),
|
||||
);
|
||||
}
|
||||
|
||||
data.insert("arch", to_json(arch));
|
||||
data.insert("bundle_id", to_json(bundle_id));
|
||||
data.insert("manufacturer", to_json(manufacturer));
|
||||
data.insert("product_name", to_json(settings.product_name()));
|
||||
data.insert("short_description", to_json(settings.short_description()));
|
||||
data.insert(
|
||||
"homepage",
|
||||
to_json(settings.homepage_url().unwrap_or_default()),
|
||||
);
|
||||
data.insert(
|
||||
"long_description",
|
||||
to_json(settings.long_description().unwrap_or_default()),
|
||||
);
|
||||
data.insert("copyright", to_json(settings.copyright_string()));
|
||||
|
||||
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();
|
||||
data.insert("version", to_json(version));
|
||||
data.insert(
|
||||
"version_with_build",
|
||||
to_json(try_add_numeric_build_number(version)?),
|
||||
);
|
||||
|
||||
data.insert(
|
||||
"allow_downgrades",
|
||||
to_json(settings.windows().allow_downgrades),
|
||||
);
|
||||
|
||||
if let Some(license_file) = settings.license_file() {
|
||||
let license_file = dunce::canonicalize(&license_file)
|
||||
.fs_context("failed to resolve `bundle > licenseFile`", license_file)?;
|
||||
let license_file_with_bom = output_path.join("license_file");
|
||||
let content = std::fs::read(license_file)?;
|
||||
write_utf8_with_bom(&license_file_with_bom, content)?;
|
||||
data.insert("license", to_json(license_file_with_bom));
|
||||
}
|
||||
|
||||
let nsis = settings.windows().nsis.as_ref();
|
||||
|
||||
let custom_template_path = nsis.as_ref().and_then(|n| n.template.clone());
|
||||
|
||||
let install_mode = nsis
|
||||
.as_ref()
|
||||
.map(|n| n.install_mode)
|
||||
.unwrap_or(NSISInstallerMode::CurrentUser);
|
||||
|
||||
if let Some(nsis) = nsis {
|
||||
if let Some(installer_icon) = &nsis.installer_icon {
|
||||
data.insert(
|
||||
"installer_icon",
|
||||
to_json(dunce::canonicalize(installer_icon).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > installerIcon`",
|
||||
installer_icon.to_owned(),
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(header_image) = &nsis.header_image {
|
||||
data.insert(
|
||||
"header_image",
|
||||
to_json(dunce::canonicalize(header_image).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > headerImage`",
|
||||
header_image.to_owned(),
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(sidebar_image) = &nsis.sidebar_image {
|
||||
data.insert(
|
||||
"sidebar_image",
|
||||
to_json(dunce::canonicalize(sidebar_image).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > sidebarImage`",
|
||||
sidebar_image.to_owned(),
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(uninstaller_icon) = &nsis.uninstaller_icon {
|
||||
data.insert(
|
||||
"uninstaller_icon",
|
||||
to_json(dunce::canonicalize(uninstaller_icon).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > uninstallerIcon`",
|
||||
uninstaller_icon.to_owned(),
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(uninstaller_header_image) = &nsis.uninstaller_header_image {
|
||||
data.insert(
|
||||
"uninstaller_header_image",
|
||||
to_json(dunce::canonicalize(uninstaller_header_image).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > uninstallerHeaderImage`",
|
||||
uninstaller_header_image.to_owned(),
|
||||
)?),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(installer_hooks) = &nsis.installer_hooks {
|
||||
let installer_hooks = dunce::canonicalize(installer_hooks).fs_context(
|
||||
"failed to resolve `bundle > windows > nsis > installerHooks`",
|
||||
installer_hooks.to_owned(),
|
||||
)?;
|
||||
data.insert("installer_hooks", to_json(installer_hooks));
|
||||
}
|
||||
|
||||
if let Some(start_menu_folder) = &nsis.start_menu_folder {
|
||||
data.insert("start_menu_folder", to_json(start_menu_folder));
|
||||
}
|
||||
#[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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let compression = settings
|
||||
.windows()
|
||||
.nsis
|
||||
.as_ref()
|
||||
.map(|n| n.compression)
|
||||
.unwrap_or_default();
|
||||
data.insert(
|
||||
"compression",
|
||||
to_json(match compression {
|
||||
NsisCompression::Zlib => "zlib",
|
||||
NsisCompression::Bzip2 => "bzip2",
|
||||
NsisCompression::Lzma => "lzma",
|
||||
NsisCompression::None => "none",
|
||||
}),
|
||||
);
|
||||
|
||||
data.insert(
|
||||
"install_mode",
|
||||
to_json(match install_mode {
|
||||
NSISInstallerMode::CurrentUser => "currentUser",
|
||||
NSISInstallerMode::PerMachine => "perMachine",
|
||||
NSISInstallerMode::Both => "both",
|
||||
}),
|
||||
);
|
||||
|
||||
let languages = nsis
|
||||
.and_then(|nsis| nsis.languages.clone())
|
||||
.unwrap_or_else(|| vec!["English".into()]);
|
||||
data.insert("languages", to_json(languages.clone()));
|
||||
|
||||
data.insert(
|
||||
"display_language_selector",
|
||||
to_json(
|
||||
nsis
|
||||
.map(|nsis| nsis.display_language_selector && languages.len() > 1)
|
||||
.unwrap_or(false),
|
||||
),
|
||||
);
|
||||
|
||||
let custom_language_files = nsis.and_then(|nsis| nsis.custom_language_files.clone());
|
||||
|
||||
let mut language_files_paths = Vec::new();
|
||||
for lang in &languages {
|
||||
// if user provided a custom lang file, we rewrite it with BOM
|
||||
if let Some(path) = custom_language_files.as_ref().and_then(|h| h.get(lang)) {
|
||||
let path = dunce::canonicalize(path)?;
|
||||
let path_with_bom = path
|
||||
.file_name()
|
||||
.map(|f| output_path.join(f))
|
||||
.unwrap_or_else(|| output_path.join(format!("{lang}_custom.nsh")));
|
||||
let content = std::fs::read(path)?;
|
||||
write_utf8_with_bom(&path_with_bom, content)?;
|
||||
language_files_paths.push(path_with_bom);
|
||||
} else {
|
||||
// if user has not provided a custom lang file,
|
||||
// we check our translated languages
|
||||
if let Some((file_name, content)) = get_lang_data(lang) {
|
||||
let path = output_path.join(file_name);
|
||||
write_utf8_with_bom(&path, content)?;
|
||||
language_files_paths.push(path);
|
||||
} else {
|
||||
log::warn!(
|
||||
"Custom tauri messages for {lang} are not translated.\nIf it is a valid language listed on <https://github.com/kichik/nsis/tree/9465c08046f00ccb6eda985abbdbf52c275c6c4d/Contrib/Language%20files>, please open a Tauri feature request\n or you can provide a custom language file for it in `tauri.conf.json > bundle > windows > nsis > custom_language_files`"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.insert("language_files", to_json(language_files_paths));
|
||||
|
||||
let main_binary = settings.main_binary()?;
|
||||
let main_binary_path = settings.binary_path(main_binary);
|
||||
data.insert("main_binary_name", to_json(main_binary.name()));
|
||||
data.insert("main_binary_path", to_json(&main_binary_path));
|
||||
|
||||
let out_file = "nsis-output.exe";
|
||||
data.insert("out_file", to_json(out_file));
|
||||
|
||||
let resources = generate_resource_data(settings)?;
|
||||
let resources_dirs =
|
||||
std::collections::HashSet::<PathBuf>::from_iter(resources.values().map(|r| r.0.to_owned()));
|
||||
|
||||
let mut resources_ancestors = resources_dirs
|
||||
.iter()
|
||||
.flat_map(|p| p.ancestors())
|
||||
.collect::<Vec<_>>();
|
||||
resources_ancestors.sort_unstable();
|
||||
resources_ancestors.dedup();
|
||||
resources_ancestors.sort_by_key(|p| std::cmp::Reverse(p.components().count()));
|
||||
resources_ancestors.pop(); // Last one is always ""
|
||||
|
||||
// We need to convert / to \ for nsis to move the files into the correct dirs
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let resources: ResourcesMap = resources
|
||||
.into_iter()
|
||||
.map(|(r, p)| {
|
||||
(
|
||||
r,
|
||||
(
|
||||
p.0.display().to_string().replace('/', "\\").into(),
|
||||
p.1.display().to_string().replace('/', "\\").into(),
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let resources_ancestors: Vec<PathBuf> = resources_ancestors
|
||||
.into_iter()
|
||||
.map(|p| p.display().to_string().replace('/', "\\").into())
|
||||
.collect();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let resources_dirs: Vec<PathBuf> = resources_dirs
|
||||
.into_iter()
|
||||
.map(|p| p.display().to_string().replace('/', "\\").into())
|
||||
.collect();
|
||||
|
||||
data.insert("resources_ancestors", to_json(resources_ancestors));
|
||||
data.insert("resources_dirs", to_json(resources_dirs));
|
||||
data.insert("resources", to_json(&resources));
|
||||
|
||||
let binaries = generate_binaries_data(settings)?;
|
||||
data.insert("binaries", to_json(&binaries));
|
||||
|
||||
let estimated_size = generate_estimated_size(&main_binary_path, &binaries, &resources)?;
|
||||
data.insert("estimated_size", to_json(estimated_size));
|
||||
|
||||
if let Some(file_associations) = settings.file_associations() {
|
||||
data.insert("file_associations", to_json(file_associations));
|
||||
}
|
||||
|
||||
if let Some(protocols) = settings.deep_link_protocols() {
|
||||
let schemes = protocols
|
||||
.iter()
|
||||
.flat_map(|p| &p.schemes)
|
||||
.collect::<Vec<_>>();
|
||||
if !schemes.is_empty() {
|
||||
data.insert("deep_link_protocols", to_json(schemes));
|
||||
}
|
||||
}
|
||||
|
||||
let silent_webview2_install = if let WebviewInstallMode::DownloadBootstrapper { silent }
|
||||
| WebviewInstallMode::EmbedBootstrapper { silent }
|
||||
| WebviewInstallMode::OfflineInstaller { silent } =
|
||||
settings.windows().webview_install_mode
|
||||
{
|
||||
silent
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
let webview2_install_mode = if updater {
|
||||
WebviewInstallMode::DownloadBootstrapper {
|
||||
silent: silent_webview2_install,
|
||||
}
|
||||
} else {
|
||||
settings.windows().webview_install_mode.clone()
|
||||
};
|
||||
|
||||
let webview2_installer_args = to_json(if silent_webview2_install {
|
||||
"/silent"
|
||||
} else {
|
||||
""
|
||||
});
|
||||
|
||||
data.insert("webview2_installer_args", to_json(webview2_installer_args));
|
||||
data.insert(
|
||||
"install_webview2_mode",
|
||||
to_json(match webview2_install_mode {
|
||||
WebviewInstallMode::DownloadBootstrapper { silent: _ } => "downloadBootstrapper",
|
||||
WebviewInstallMode::EmbedBootstrapper { silent: _ } => "embedBootstrapper",
|
||||
WebviewInstallMode::OfflineInstaller { silent: _ } => "offlineInstaller",
|
||||
_ => "",
|
||||
}),
|
||||
);
|
||||
|
||||
match webview2_install_mode {
|
||||
WebviewInstallMode::EmbedBootstrapper { silent: _ } => {
|
||||
let webview2_bootstrapper_path = download_webview2_bootstrapper(tauri_tools_path)?;
|
||||
data.insert(
|
||||
"webview2_bootstrapper_path",
|
||||
to_json(webview2_bootstrapper_path),
|
||||
);
|
||||
}
|
||||
WebviewInstallMode::OfflineInstaller { silent: _ } => {
|
||||
let webview2_installer_path =
|
||||
download_webview2_offline_installer(&tauri_tools_path.join(arch), arch)?;
|
||||
data.insert("webview2_installer_path", to_json(webview2_installer_path));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let mut handlebars = Handlebars::new();
|
||||
handlebars.register_helper("or", Box::new(handlebars_or));
|
||||
handlebars.register_helper("association-description", Box::new(association_description));
|
||||
handlebars.register_helper("no-escape", Box::new(handlebars_no_escape));
|
||||
handlebars.register_escape_fn(|s| {
|
||||
let mut output = String::new();
|
||||
for c in s.chars() {
|
||||
match c {
|
||||
'\"' => output.push_str("$\\\""),
|
||||
'$' => output.push_str("$$"),
|
||||
'`' => output.push_str("$\\`"),
|
||||
'\n' => output.push_str("$\\n"),
|
||||
'\t' => output.push_str("$\\t"),
|
||||
'\r' => output.push_str("$\\r"),
|
||||
_ => output.push(c),
|
||||
}
|
||||
}
|
||||
output
|
||||
});
|
||||
if let Some(path) = custom_template_path {
|
||||
handlebars
|
||||
.register_template_string("installer.nsi", std::fs::read_to_string(path)?)
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to setup custom handlebar template");
|
||||
} else {
|
||||
handlebars
|
||||
.register_template_string("installer.nsi", include_str!("./installer.nsi"))
|
||||
.map_err(|e| e.to_string())
|
||||
.expect("Failed to setup handlebar template");
|
||||
}
|
||||
|
||||
write_utf8_with_bom(
|
||||
output_path.join("FileAssociation.nsh"),
|
||||
include_bytes!("./FileAssociation.nsh"),
|
||||
)?;
|
||||
write_utf8_with_bom(output_path.join("utils.nsh"), include_bytes!("./utils.nsh"))?;
|
||||
|
||||
let installer_nsi_path = output_path.join("installer.nsi");
|
||||
write_utf8_with_bom(
|
||||
&installer_nsi_path,
|
||||
handlebars.render("installer.nsi", &data)?,
|
||||
)?;
|
||||
|
||||
let package_base_name = format!(
|
||||
"{}_{}_{}-setup",
|
||||
settings.product_name(),
|
||||
settings.version_string(),
|
||||
arch,
|
||||
);
|
||||
|
||||
let nsis_output_path = output_path.join(out_file);
|
||||
let nsis_installer_path = settings.project_out_directory().join(format!(
|
||||
"bundle/{}/{}.exe",
|
||||
if updater {
|
||||
NSIS_UPDATER_OUTPUT_FOLDER_NAME
|
||||
} else {
|
||||
NSIS_OUTPUT_FOLDER_NAME
|
||||
},
|
||||
package_base_name
|
||||
));
|
||||
fs::create_dir_all(nsis_installer_path.parent().unwrap())?;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log::info!(action = "Running"; "makensis to produce {}", display_path(&nsis_installer_path));
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let mut nsis_cmd = Command::new(nsis_toolset_path.join("makensis.exe"));
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let mut nsis_cmd = Command::new("makensis");
|
||||
|
||||
let status = nsis_cmd
|
||||
.args(["-INPUTCHARSET", "UTF8", "-OUTPUTCHARSET", "UTF8"])
|
||||
.arg(match settings.log_level() {
|
||||
log::Level::Error => "-V1",
|
||||
log::Level::Warn => "-V2",
|
||||
log::Level::Info => "-V3",
|
||||
_ => "-V4",
|
||||
})
|
||||
.arg(installer_nsi_path)
|
||||
.env_remove("NSISDIR")
|
||||
.env_remove("NSISCONFDIR")
|
||||
.current_dir(output_path)
|
||||
.piped()
|
||||
.map_err(|error| Error::CommandFailed {
|
||||
command: "makensis.exe".to_string(),
|
||||
error,
|
||||
})?;
|
||||
if !status.success() {
|
||||
bail!("Failed to bundle app with makensis");
|
||||
}
|
||||
|
||||
fs::rename(nsis_output_path, &nsis_installer_path)?;
|
||||
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&nsis_installer_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(vec![nsis_installer_path])
|
||||
}
|
||||
|
||||
fn handlebars_or(
|
||||
h: &handlebars::Helper<'_>,
|
||||
_: &Handlebars<'_>,
|
||||
_: &handlebars::Context,
|
||||
_: &mut handlebars::RenderContext<'_, '_>,
|
||||
out: &mut dyn handlebars::Output,
|
||||
) -> handlebars::HelperResult {
|
||||
let param1 = h.param(0).unwrap().render();
|
||||
let param2 = h.param(1).unwrap();
|
||||
|
||||
out.write(&if param1.is_empty() {
|
||||
param2.render()
|
||||
} else {
|
||||
param1
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn association_description(
|
||||
h: &handlebars::Helper<'_>,
|
||||
_: &Handlebars<'_>,
|
||||
_: &handlebars::Context,
|
||||
_: &mut handlebars::RenderContext<'_, '_>,
|
||||
out: &mut dyn handlebars::Output,
|
||||
) -> handlebars::HelperResult {
|
||||
let description = h.param(0).unwrap().render();
|
||||
let ext = h.param(1).unwrap();
|
||||
|
||||
out.write(&if description.is_empty() {
|
||||
format!("{} File", ext.render().to_uppercase())
|
||||
} else {
|
||||
description
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handlebars_no_escape(
|
||||
h: &handlebars::Helper<'_>,
|
||||
_: &Handlebars<'_>,
|
||||
_: &handlebars::Context,
|
||||
_: &mut handlebars::RenderContext<'_, '_>,
|
||||
out: &mut dyn handlebars::Output,
|
||||
) -> handlebars::HelperResult {
|
||||
// get parameter from helper or throw an error
|
||||
let param = h
|
||||
.param(0)
|
||||
.ok_or(handlebars::RenderErrorReason::ParamNotFoundForIndex(
|
||||
"no-escape",
|
||||
0,
|
||||
))?;
|
||||
write!(out, "{}", param.render())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// BTreeMap<OriginalPath, (ParentOfTargetPath, TargetPath)>
|
||||
type ResourcesMap = BTreeMap<PathBuf, (PathBuf, PathBuf)>;
|
||||
fn generate_resource_data(settings: &Settings) -> crate::Result<ResourcesMap> {
|
||||
let mut resources = ResourcesMap::new();
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let mut added_resources = Vec::new();
|
||||
|
||||
// Adding WebViewer2Loader.dll in case windows-gnu toolchain is used
|
||||
if settings.target().ends_with("-gnu") {
|
||||
let loader_path =
|
||||
dunce::simplified(&settings.project_out_directory().join("WebView2Loader.dll")).to_path_buf();
|
||||
if loader_path.exists() {
|
||||
if settings.windows().can_sign() {
|
||||
try_sign(&loader_path, settings)?;
|
||||
}
|
||||
added_resources.push(loader_path.clone());
|
||||
resources.insert(
|
||||
loader_path,
|
||||
(PathBuf::new(), PathBuf::from("WebView2Loader.dll")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if settings.windows().bundle_vc_runtime {
|
||||
for dll in vc_runtime_dlls(settings.binary_arch())? {
|
||||
let dll = dunce::simplified(&dll).to_path_buf();
|
||||
if added_resources.contains(&dll) {
|
||||
continue;
|
||||
}
|
||||
let target = PathBuf::from(
|
||||
dll
|
||||
.file_name()
|
||||
.expect("failed to extract Visual C++ runtime DLL filename"),
|
||||
);
|
||||
added_resources.push(dll.clone());
|
||||
resources.insert(dll, (PathBuf::new(), target));
|
||||
}
|
||||
}
|
||||
|
||||
for resource in settings.resource_files().iter() {
|
||||
let resource = resource?;
|
||||
|
||||
let src = cwd.join(resource.path());
|
||||
let resource_path = dunce::simplified(&src).to_path_buf();
|
||||
|
||||
// In some glob resource paths like `assets/**/*` a file might appear twice
|
||||
// because the `tauri_utils::resources::ResourcePaths` iterator also reads a directory
|
||||
// when it finds one. So we must check it before processing the file.
|
||||
if added_resources.contains(&resource_path) {
|
||||
continue;
|
||||
}
|
||||
added_resources.push(resource_path.clone());
|
||||
|
||||
if settings.windows().can_sign() && should_sign(&resource_path)? {
|
||||
try_sign(&resource_path, settings)?;
|
||||
}
|
||||
|
||||
let target_path = resource.target();
|
||||
resources.insert(
|
||||
resource_path,
|
||||
(
|
||||
target_path
|
||||
.parent()
|
||||
.expect("Couldn't get parent of target path")
|
||||
.to_path_buf(),
|
||||
target_path.to_path_buf(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
/// BTreeMap<OriginalPath, TargetFileName>
|
||||
type BinariesMap = BTreeMap<PathBuf, String>;
|
||||
fn generate_binaries_data(settings: &Settings) -> crate::Result<BinariesMap> {
|
||||
let mut binaries = BinariesMap::new();
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
for src in settings.external_binaries() {
|
||||
let src = src?;
|
||||
let binary_path = dunce::canonicalize(cwd.join(&src))?;
|
||||
let dest_filename = src
|
||||
.file_name()
|
||||
.expect("failed to extract external binary filename")
|
||||
.to_string_lossy()
|
||||
.replace(&format!("-{}", settings.target()), "");
|
||||
binaries.insert(binary_path, dest_filename);
|
||||
}
|
||||
|
||||
for bin in settings.binaries() {
|
||||
if !bin.main() {
|
||||
let bin_path = settings.binary_path(bin);
|
||||
binaries.insert(
|
||||
bin_path.clone(),
|
||||
bin_path
|
||||
.file_name()
|
||||
.expect("failed to extract external binary filename")
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(binaries)
|
||||
}
|
||||
|
||||
fn generate_estimated_size(
|
||||
main: &PathBuf,
|
||||
binaries: &BinariesMap,
|
||||
resources: &ResourcesMap,
|
||||
) -> crate::Result<u64> {
|
||||
let mut size = 0;
|
||||
for k in std::iter::once(main)
|
||||
.chain(binaries.keys())
|
||||
.chain(resources.keys())
|
||||
{
|
||||
size += std::fs::metadata(k)
|
||||
.map_err(|error| Error::Fs {
|
||||
context: "when getting size of",
|
||||
path: k.to_path_buf(),
|
||||
error,
|
||||
})?
|
||||
.len();
|
||||
}
|
||||
Ok(size / 1024)
|
||||
}
|
||||
|
||||
fn get_lang_data(lang: &str) -> Option<(String, &[u8])> {
|
||||
let path = format!("{lang}.nsh");
|
||||
let content: &[u8] = match lang.to_lowercase().as_str() {
|
||||
"arabic" => include_bytes!("./languages/Arabic.nsh"),
|
||||
"bulgarian" => include_bytes!("./languages/Bulgarian.nsh"),
|
||||
"dutch" => include_bytes!("./languages/Dutch.nsh"),
|
||||
"english" => include_bytes!("./languages/English.nsh"),
|
||||
"german" => include_bytes!("./languages/German.nsh"),
|
||||
"italian" => include_bytes!("./languages/Italian.nsh"),
|
||||
"japanese" => include_bytes!("./languages/Japanese.nsh"),
|
||||
"korean" => include_bytes!("./languages/Korean.nsh"),
|
||||
"portuguesebr" => include_bytes!("./languages/PortugueseBR.nsh"),
|
||||
"russian" => include_bytes!("./languages/Russian.nsh"),
|
||||
"tradchinese" => include_bytes!("./languages/TradChinese.nsh"),
|
||||
"simpchinese" => include_bytes!("./languages/SimpChinese.nsh"),
|
||||
"french" => include_bytes!("./languages/French.nsh"),
|
||||
"spanish" => include_bytes!("./languages/Spanish.nsh"),
|
||||
"spanishinternational" => include_bytes!("./languages/SpanishInternational.nsh"),
|
||||
"persian" => include_bytes!("./languages/Persian.nsh"),
|
||||
"turkish" => include_bytes!("./languages/Turkish.nsh"),
|
||||
"swedish" => include_bytes!("./languages/Swedish.nsh"),
|
||||
"portuguese" => include_bytes!("./languages/Portuguese.nsh"),
|
||||
"ukrainian" => include_bytes!("./languages/Ukrainian.nsh"),
|
||||
"norwegian" => include_bytes!("./languages/Norwegian.nsh"),
|
||||
"vietnamese" => include_bytes!("./languages/Vietnamese.nsh"),
|
||||
_ => return None,
|
||||
};
|
||||
Some((path, content))
|
||||
}
|
||||
|
||||
fn write_utf8_with_bom<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, content: C) -> crate::Result<()> {
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
let file = File::create(path)?;
|
||||
let mut output = BufWriter::new(file);
|
||||
output.write_all(&[0xEF, 0xBB, 0xBF])?; // UTF-8 BOM
|
||||
output.write_all(content.as_ref())?;
|
||||
Ok(())
|
||||
}
|
||||
188
crates/tauri-bundler/src/bundle/windows/nsis/utils.nsh
Normal file
188
crates/tauri-bundler/src/bundle/windows/nsis/utils.nsh
Normal file
@@ -0,0 +1,188 @@
|
||||
; Change shell and registry context based on running
|
||||
; architecture and chosen install mode.
|
||||
!macro SetContext
|
||||
!if "${INSTALLMODE}" == "currentUser"
|
||||
SetShellVarContext current
|
||||
!else if "${INSTALLMODE}" == "perMachine"
|
||||
SetShellVarContext all
|
||||
!endif
|
||||
|
||||
${If} ${RunningX64}
|
||||
!if "${ARCH}" == "x64"
|
||||
SetRegView 64
|
||||
!else if "${ARCH}" == "arm64"
|
||||
SetRegView 64
|
||||
!else
|
||||
SetRegView 32
|
||||
!endif
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!define /ifndef ERROR_MORE_DATA 234
|
||||
|
||||
; Checks whether app is running or not and prompts to kill it.
|
||||
!macro CheckIfAppIsRunning executablePath productName
|
||||
!define UniqueID ${__LINE__}
|
||||
|
||||
; Replace {{product_name}} placeholder in the messages with the passed product name
|
||||
nsis_tauri_utils::StrReplace "$(appRunning)" "{{product_name}}" "${productName}"
|
||||
Pop $R1
|
||||
nsis_tauri_utils::StrReplace "$(appRunningOkKill)" "{{product_name}}" "${productName}"
|
||||
Pop $R2
|
||||
nsis_tauri_utils::StrReplace "$(failedToKillApp)" "{{product_name}}" "${productName}"
|
||||
Pop $R3
|
||||
|
||||
!insertmacro RestartManager_StartSession $R0
|
||||
${If} $R0 != ""
|
||||
!insertmacro RestartManager_RegisterFile $R0 "${executablePath}"
|
||||
${If} $0 = 0
|
||||
System::Call 'RSTRTMGR::RmGetList(p R0, *i .r1, *i .r2, p 0, *i .r3) i .r0'
|
||||
; ERROR_SUCCESS means no running processes, ERROR_MORE_DATA means there're some
|
||||
${If} $0 = ${ERROR_MORE_DATA}
|
||||
IfSilent kill_${UniqueID} 0
|
||||
${IfThen} $PassiveMode != 1 ${|} MessageBox MB_OKCANCEL $R2 IDOK kill_${UniqueID} IDCANCEL cancel_${UniqueID} ${|}
|
||||
kill_${UniqueID}:
|
||||
; Call manually here to get the result
|
||||
; !insertmacro RestartManager_Shutdown $R0
|
||||
System::Call 'RSTRTMGR::RmShutdown(p R0, i ${RmForceShutdown}, p 0) i .r0'
|
||||
${If} $0 = 0
|
||||
Goto app_check_done_${UniqueID}
|
||||
${Else}
|
||||
IfSilent silent_${UniqueID} ui_${UniqueID}
|
||||
silent_${UniqueID}:
|
||||
System::Call 'kernel32::AttachConsole(i -1)i.r0'
|
||||
${If} $0 != 0
|
||||
System::Call 'kernel32::GetStdHandle(i -11)i.r0'
|
||||
System::call 'kernel32::SetConsoleTextAttribute(i r0, i 0x0004)' ; set red color
|
||||
FileWrite $0 "$R1$\n"
|
||||
${EndIf}
|
||||
!insertmacro RestartManager_EndSession $R0
|
||||
Abort
|
||||
ui_${UniqueID}:
|
||||
!insertmacro RestartManager_EndSession $R0
|
||||
Abort $R3
|
||||
${EndIf}
|
||||
cancel_${UniqueID}:
|
||||
!insertmacro RestartManager_EndSession $R0
|
||||
Abort $R1
|
||||
app_check_done_${UniqueID}:
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!insertmacro RestartManager_EndSession $R0
|
||||
${EndIf}
|
||||
|
||||
!undef UniqueID
|
||||
!macroend
|
||||
|
||||
; Sets AppUserModelId on a shortcut
|
||||
!macro SetLnkAppUserModelId shortcut
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 ""
|
||||
${If} $0 P<> 0
|
||||
${IUnknown::QueryInterface} $0 '("${IID_IPersistFile}",.r1)'
|
||||
${If} $1 P<> 0
|
||||
${IPersistFile::Load} $1 '("${shortcut}", ${STGM_READWRITE})'
|
||||
${IUnknown::QueryInterface} $0 '("${IID_IPropertyStore}",.r2)'
|
||||
${If} $2 P<> 0
|
||||
System::Call 'Oleaut32::SysAllocString(w "${BUNDLEID}") i.r3'
|
||||
System::Call '*${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ID})p.r4'
|
||||
System::Call '*${SYSSTRUCT_PROPVARIANT}(${VT_BSTR},,&i4 $3)p.r5'
|
||||
${IPropertyStore::SetValue} $2 '($4,$5)'
|
||||
|
||||
System::Call 'Oleaut32::SysFreeString($3)'
|
||||
System::Free $4
|
||||
System::Free $5
|
||||
${IPropertyStore::Commit} $2 ""
|
||||
${IUnknown::Release} $2 ""
|
||||
${IPersistFile::Save} $1 '("${shortcut}",1)'
|
||||
${EndIf}
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
${IUnknown::Release} $0 ""
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
; Deletes jump list entries and recent destinations
|
||||
!macro DeleteAppUserModelId
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_DestinationList} ${IID_ICustomDestinationList} r1 ""
|
||||
${If} $1 P<> 0
|
||||
${ICustomDestinationList::DeleteList} $1 '("${BUNDLEID}")'
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ApplicationDestinations} ${IID_IApplicationDestinations} r1 ""
|
||||
${If} $1 P<> 0
|
||||
${IApplicationDestinations::SetAppID} $1 '("${BUNDLEID}")i.r0'
|
||||
${If} $0 >= 0
|
||||
${IApplicationDestinations::RemoveAllDestinations} $1 ''
|
||||
${EndIf}
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
; Unpins a shortcut from Start menu and Taskbar
|
||||
;
|
||||
; From https://stackoverflow.com/a/42816728/16993372
|
||||
!macro UnpinShortcut shortcut
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_StartMenuPin} ${IID_IStartMenuPinnedList} r0 ""
|
||||
${If} $0 P<> 0
|
||||
System::Call 'SHELL32::SHCreateItemFromParsingName(ws, p0, g "${IID_IShellItem}", *p0r1)' "${shortcut}"
|
||||
${If} $1 P<> 0
|
||||
${IStartMenuPinnedList::RemoveFromList} $0 '(r1)'
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
${IUnknown::Release} $0 ""
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
; Set target path for a .lnk shortcut
|
||||
!macro SetShortcutTarget shortcut target
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 ""
|
||||
${If} $0 P<> 0
|
||||
${IUnknown::QueryInterface} $0 '("${IID_IPersistFile}",.r1)'
|
||||
${If} $1 P<> 0
|
||||
${IPersistFile::Load} $1 '("${shortcut}", ${STGM_READWRITE})'
|
||||
${IShellLink::SetPath} $0 '(w "${target}")'
|
||||
${IPersistFile::Save} $1 '("${shortcut}",1)'
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
${IUnknown::Release} $0 ""
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
!define /ifndef MAX_PATH 260
|
||||
!define /ifndef SLGP_RAWPATH 0x4
|
||||
|
||||
; Test if a .lnk shortcut's target is target,
|
||||
; use Pop to get the result, 1 is yes, 0 is no,
|
||||
; note that this macro modifies $0, $1, $2, $3
|
||||
;
|
||||
; Example usage:
|
||||
; !insertmacro "IsShortCutTarget" "C:\Users\Public\Desktop\App.lnk" "C:\Program Files\App\App.exe"
|
||||
; Pop $0
|
||||
; ${If} $0 = 1
|
||||
; MessageBox MB_OK "shortcut target matches"
|
||||
; ${EndIf}
|
||||
!macro IsShortcutTarget shortcut target
|
||||
; $0: IShellLink
|
||||
; $1: IPersistFile
|
||||
; $2: Target path
|
||||
; $3: Return value
|
||||
|
||||
StrCpy $3 0
|
||||
!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r0 ""
|
||||
${If} $0 P<> 0
|
||||
${IUnknown::QueryInterface} $0 '("${IID_IPersistFile}", .r1)'
|
||||
${If} $1 P<> 0
|
||||
${IPersistFile::Load} $1 '("${shortcut}", ${STGM_READ})'
|
||||
System::Alloc MAX_PATH
|
||||
Pop $2
|
||||
${IShellLink::GetPath} $0 '(.r2, ${MAX_PATH}, 0, ${SLGP_RAWPATH})'
|
||||
${If} $2 == "${target}"
|
||||
StrCpy $3 1
|
||||
${EndIf}
|
||||
System::Free $2
|
||||
${IUnknown::Release} $1 ""
|
||||
${EndIf}
|
||||
${IUnknown::Release} $0 ""
|
||||
${EndIf}
|
||||
Push $3
|
||||
!macroend
|
||||
284
crates/tauri-bundler/src/bundle/windows/sign.rs
Normal file
284
crates/tauri-bundler/src/bundle/windows/sign.rs
Normal file
@@ -0,0 +1,284 @@
|
||||
// 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
|
||||
|
||||
use crate::bundle::settings::CustomSignCommandSettings;
|
||||
#[cfg(windows)]
|
||||
use crate::bundle::windows::util;
|
||||
use crate::{Settings, utils::CommandExt};
|
||||
#[cfg(windows)]
|
||||
use std::path::PathBuf;
|
||||
#[cfg(windows)]
|
||||
use std::sync::OnceLock;
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
impl Settings {
|
||||
pub(crate) fn sign_params(&self) -> SignParams {
|
||||
SignParams {
|
||||
product_name: self.product_name().into(),
|
||||
digest_algorithm: self
|
||||
.windows()
|
||||
.digest_algorithm
|
||||
.as_ref()
|
||||
.map(|algorithm| algorithm.to_string())
|
||||
.unwrap_or_else(|| "sha256".to_string()),
|
||||
certificate_thumbprint: self
|
||||
.windows()
|
||||
.certificate_thumbprint
|
||||
.clone()
|
||||
.unwrap_or_default(),
|
||||
timestamp_url: self
|
||||
.windows()
|
||||
.timestamp_url
|
||||
.as_ref()
|
||||
.map(|url| url.to_string()),
|
||||
tsp: self.windows().tsp,
|
||||
sign_command: self.windows().sign_command.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(not(windows), allow(dead_code))]
|
||||
pub struct SignParams {
|
||||
pub product_name: String,
|
||||
pub digest_algorithm: String,
|
||||
pub certificate_thumbprint: String,
|
||||
pub timestamp_url: Option<String>,
|
||||
pub tsp: bool,
|
||||
pub sign_command: Option<CustomSignCommandSettings>,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn signtool() -> Option<PathBuf> {
|
||||
// sign code forked from https://github.com/forbjok/rust-codesign
|
||||
static SIGN_TOOL: OnceLock<crate::Result<PathBuf>> = OnceLock::new();
|
||||
SIGN_TOOL
|
||||
.get_or_init(|| {
|
||||
if let Some(signtool) = std::env::var_os("TAURI_WINDOWS_SIGNTOOL_PATH") {
|
||||
return Ok(PathBuf::from(signtool));
|
||||
}
|
||||
|
||||
const INSTALLED_ROOTS_REGKEY_PATH: &str = r"SOFTWARE\Microsoft\Windows Kits\Installed Roots";
|
||||
const KITS_ROOT_REGVALUE_NAME: &str = r"KitsRoot10";
|
||||
|
||||
// Open 32-bit HKLM "Installed Roots" key
|
||||
let installed_roots_key = windows_registry::LOCAL_MACHINE
|
||||
.open(INSTALLED_ROOTS_REGKEY_PATH)
|
||||
.map_err(|_| crate::Error::OpenRegistry(INSTALLED_ROOTS_REGKEY_PATH.to_string()))?;
|
||||
|
||||
// Get the Windows SDK root path
|
||||
let kits_root_10_path: String = installed_roots_key
|
||||
.get_string(KITS_ROOT_REGVALUE_NAME)
|
||||
.map_err(|_| crate::Error::GetRegistryValue(KITS_ROOT_REGVALUE_NAME.to_string()))?;
|
||||
|
||||
// Construct Windows SDK bin path
|
||||
let kits_root_10_bin_path = Path::new(&kits_root_10_path).join("bin");
|
||||
|
||||
let mut installed_kits: Vec<String> = installed_roots_key
|
||||
.keys()
|
||||
.map_err(|_| crate::Error::FailedToEnumerateRegKeys)?
|
||||
.collect();
|
||||
|
||||
// Sort installed kits
|
||||
installed_kits.sort();
|
||||
|
||||
/* Iterate through installed kit version keys in reverse (from newest to oldest),
|
||||
adding their bin paths to the list.
|
||||
Windows SDK 10 v10.0.15063.468 and later will have their signtools located there. */
|
||||
let mut kit_bin_paths: Vec<PathBuf> = installed_kits
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|kit| kits_root_10_bin_path.join(kit))
|
||||
.collect();
|
||||
|
||||
/* Add kits root bin path.
|
||||
For Windows SDK 10 versions earlier than v10.0.15063.468, signtool will be located there. */
|
||||
kit_bin_paths.push(kits_root_10_bin_path);
|
||||
|
||||
// Choose which version of SignTool to use based on OS bitness
|
||||
let arch_dir = util::processor_architecture().ok_or(crate::Error::UnsupportedBitness)?;
|
||||
|
||||
/* Iterate through all bin paths, checking for existence of a SignTool executable. */
|
||||
for kit_bin_path in &kit_bin_paths {
|
||||
/* Construct SignTool path. */
|
||||
let signtool_path = kit_bin_path.join(arch_dir).join("signtool.exe");
|
||||
|
||||
/* Check if SignTool exists at this location. */
|
||||
if signtool_path.exists() {
|
||||
// SignTool found. Return it.
|
||||
return Ok(signtool_path);
|
||||
}
|
||||
}
|
||||
|
||||
Err(crate::Error::SignToolNotFound)
|
||||
})
|
||||
.as_ref()
|
||||
.ok()
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Check if binary is already signed.
|
||||
/// Used to skip sidecar binaries that are already signed.
|
||||
#[cfg(windows)]
|
||||
pub fn verify(path: &Path) -> crate::Result<bool> {
|
||||
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
|
||||
|
||||
let mut cmd = Command::new(signtool);
|
||||
cmd.arg("verify");
|
||||
cmd.arg("/pa");
|
||||
cmd.arg(path);
|
||||
|
||||
Ok(cmd.status()?.success())
|
||||
}
|
||||
|
||||
pub fn sign_command_custom<P: AsRef<Path>>(
|
||||
path: P,
|
||||
command: &CustomSignCommandSettings,
|
||||
) -> crate::Result<Command> {
|
||||
let path = path.as_ref();
|
||||
|
||||
let cwd = std::env::current_dir()?;
|
||||
|
||||
let mut cmd = Command::new(&command.cmd);
|
||||
for arg in &command.args {
|
||||
if arg == "%1" {
|
||||
cmd.arg(path);
|
||||
} else {
|
||||
let path = Path::new(arg);
|
||||
// turn relative paths into absolute paths - so the uninstall command can use them
|
||||
// since the !uninstfinalize NSIS hook runs in a different directory
|
||||
if path.exists() && path.is_relative() {
|
||||
cmd.arg(cwd.join(path));
|
||||
} else {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn sign_command_default<P: AsRef<Path>>(
|
||||
path: P,
|
||||
params: &SignParams,
|
||||
) -> crate::Result<Command> {
|
||||
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
|
||||
|
||||
let mut cmd = Command::new(signtool);
|
||||
cmd.arg("sign");
|
||||
cmd.args(["/fd", ¶ms.digest_algorithm]);
|
||||
cmd.args(["/sha1", ¶ms.certificate_thumbprint]);
|
||||
cmd.args(["/d", ¶ms.product_name]);
|
||||
|
||||
if let Some(ref timestamp_url) = params.timestamp_url {
|
||||
if params.tsp {
|
||||
cmd.args(["/tr", timestamp_url]);
|
||||
cmd.args(["/td", ¶ms.digest_algorithm]);
|
||||
} else {
|
||||
cmd.args(["/t", timestamp_url]);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.arg(path.as_ref());
|
||||
|
||||
Ok(cmd)
|
||||
}
|
||||
|
||||
pub fn sign_command<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<Command> {
|
||||
match ¶ms.sign_command {
|
||||
Some(custom_command) => sign_command_custom(path, custom_command),
|
||||
#[cfg(windows)]
|
||||
None => sign_command_default(path, params),
|
||||
|
||||
// should not be reachable
|
||||
#[cfg(not(windows))]
|
||||
None => Ok(Command::new("")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sign_custom<P: AsRef<Path>>(
|
||||
path: P,
|
||||
custom_command: &CustomSignCommandSettings,
|
||||
) -> crate::Result<()> {
|
||||
let path = path.as_ref();
|
||||
|
||||
log::info!(action = "Signing";"{} with a custom signing command", tauri_utils::display_path(path));
|
||||
|
||||
let mut cmd = sign_command_custom(path, custom_command)?;
|
||||
|
||||
let output = cmd.output_ok()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
|
||||
log::info!(action = "Signing";"Output of signing command:\n{}", stdout.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn sign_default<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
|
||||
let signtool = signtool().ok_or(crate::Error::SignToolNotFound)?;
|
||||
let path = path.as_ref();
|
||||
|
||||
log::info!(action = "Signing"; "{} with identity \"{}\"", tauri_utils::display_path(path), params.certificate_thumbprint);
|
||||
|
||||
let mut cmd = sign_command_default(path, params)?;
|
||||
log::debug!("Running signtool {:?}", signtool);
|
||||
|
||||
// Execute SignTool command
|
||||
let output = cmd.output_ok()?;
|
||||
|
||||
let stdout = String::from_utf8_lossy(output.stdout.as_slice()).into_owned();
|
||||
log::info!(action = "Signing";"Output of signing command:\n{}", stdout.trim());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sign<P: AsRef<Path>>(path: P, params: &SignParams) -> crate::Result<()> {
|
||||
match ¶ms.sign_command {
|
||||
Some(custom_command) => sign_custom(path, custom_command),
|
||||
#[cfg(windows)]
|
||||
None => sign_default(path, params),
|
||||
// should not be reachable, as user should either use Windows
|
||||
// or specify a custom sign_command but we succeed anyways
|
||||
#[cfg(not(windows))]
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_sign<P: AsRef<Path>>(file_path: P, settings: &Settings) -> crate::Result<()> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
247
crates/tauri-bundler/src/bundle/windows/util.rs
Normal file
247
crates/tauri-bundler/src/bundle/windows/util.rs
Normal file
@@ -0,0 +1,247 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::process::Command;
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use ureq::ResponseExt;
|
||||
|
||||
use crate::bundle::settings::Arch;
|
||||
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 =
|
||||
"https://go.microsoft.com/fwlink/?linkid=2099617";
|
||||
pub const WEBVIEW2_OFFLINE_INSTALLER_X64_URL: &str =
|
||||
"https://go.microsoft.com/fwlink/?linkid=2124701";
|
||||
pub const WEBVIEW2_URL_PREFIX: &str =
|
||||
"https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/";
|
||||
pub const NSIS_OUTPUT_FOLDER_NAME: &str = "nsis";
|
||||
pub const NSIS_UPDATER_OUTPUT_FOLDER_NAME: &str = "nsis-updater";
|
||||
pub const WIX_OUTPUT_FOLDER_NAME: &str = "msi";
|
||||
pub const WIX_UPDATER_OUTPUT_FOLDER_NAME: &str = "msi-updater";
|
||||
|
||||
const VSWHERE: &[u8] = include_bytes!("vswhere.exe");
|
||||
const VCTOOLS_REDIST_DIR_ENV_VAR: &str = "VCTOOLS_REDIST_DIR";
|
||||
#[cfg(windows)]
|
||||
const VC_REDIST_COMPONENT: &str = "Microsoft.VisualStudio.Component.VC.Redist.14.Latest";
|
||||
|
||||
pub fn webview2_guid_path(url: &str) -> crate::Result<(String, String)> {
|
||||
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(|| {
|
||||
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(|| {
|
||||
crate::Error::GenericError(format!(
|
||||
"WebView2 URL format mismatch. Expected `<GUID>/<FILENAME>`, found `{remaining_url}`."
|
||||
))
|
||||
})?;
|
||||
Ok((guid.into(), filename.into()))
|
||||
}
|
||||
|
||||
pub fn download_webview2_bootstrapper(base_path: &Path) -> crate::Result<PathBuf> {
|
||||
let file_path = base_path.join("MicrosoftEdgeWebview2Setup.exe");
|
||||
if !file_path.exists() {
|
||||
std::fs::write(&file_path, download(WEBVIEW2_BOOTSTRAPPER_URL)?)?;
|
||||
}
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
pub fn download_webview2_offline_installer(base_path: &Path, arch: &str) -> crate::Result<PathBuf> {
|
||||
let url = if arch == "x64" {
|
||||
WEBVIEW2_OFFLINE_INSTALLER_X64_URL
|
||||
} else {
|
||||
WEBVIEW2_OFFLINE_INSTALLER_X86_URL
|
||||
};
|
||||
let (guid, filename) = webview2_guid_path(url)?;
|
||||
let dir_path = base_path.join(guid);
|
||||
let file_path = dir_path.join(filename);
|
||||
if !file_path.exists() {
|
||||
fs::create_dir_all(dir_path)?;
|
||||
std::fs::write(&file_path, download(url)?)?;
|
||||
}
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// Finds the Visual C++ runtime DLLs for the given architecture.
|
||||
pub fn vc_runtime_dlls(arch: Arch) -> crate::Result<Vec<PathBuf>> {
|
||||
let arch = vc_runtime_arch(arch)?;
|
||||
let redist_dir = vc_redist_dir()?;
|
||||
let runtime_dir = vc_runtime_dir(&redist_dir, arch)?;
|
||||
|
||||
let dlls = glob::glob(&glob_path(&runtime_dir, "*.dll"))?.collect::<Result<Vec<_>, _>>()?;
|
||||
if dlls.is_empty() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"no Visual C++ runtime DLLs found in `{}`",
|
||||
runtime_dir.display()
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(dlls)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn vc_runtime_arch(arch: Arch) -> crate::Result<&'static str> {
|
||||
match arch {
|
||||
Arch::X86_64 => Ok("x64"),
|
||||
Arch::X86 => Ok("x86"),
|
||||
Arch::AArch64 => Ok("arm64"),
|
||||
_ => Err(crate::Error::GenericError(
|
||||
"bundling the Visual C++ runtime is only supported for Windows x86, x64 and arm64 targets"
|
||||
.into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn visual_studio_dir() -> crate::Result<PathBuf> {
|
||||
let Some(vswhere) = vswhere_path() else {
|
||||
return Err(crate::Error::GenericError(
|
||||
"failed to prepare bundled vswhere.exe".into(),
|
||||
));
|
||||
};
|
||||
|
||||
let output = Command::new(vswhere)
|
||||
.args([
|
||||
"-latest",
|
||||
"-prerelease",
|
||||
"-products",
|
||||
"*",
|
||||
"-requires",
|
||||
VC_REDIST_COMPONENT,
|
||||
"-property",
|
||||
"installationPath",
|
||||
"-format",
|
||||
"value",
|
||||
"-utf8",
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component"
|
||||
)));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let Some(vs_dir) = stdout.lines().map(str::trim).find(|line| !line.is_empty()) else {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"failed to locate Visual Studio with the {VC_REDIST_COMPONENT} component"
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(PathBuf::from(vs_dir))
|
||||
}
|
||||
|
||||
fn vc_redist_dir() -> crate::Result<PathBuf> {
|
||||
if let Ok(redist_dir) = std::env::var(VCTOOLS_REDIST_DIR_ENV_VAR) {
|
||||
return Ok(PathBuf::from(redist_dir));
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let vs_dir = visual_studio_dir()?;
|
||||
Ok(vs_dir.join("VC/Redist/MSVC"))
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Err(crate::Error::GenericError(format!(
|
||||
"failed to find Visual C++ runtime redist directory; set {VCTOOLS_REDIST_DIR_ENV_VAR} when bundling the Visual C++ runtime from non-Windows hosts"
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
fn vc_runtime_dir(redist_dir: &Path, arch: &str) -> crate::Result<PathBuf> {
|
||||
let Some(latest_version_dir) = latest_vc_redist_version_dir(redist_dir)? else {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"failed to find Visual C++ runtime versions in `{}`",
|
||||
redist_dir.display()
|
||||
)));
|
||||
};
|
||||
|
||||
let arch_dir = latest_version_dir.join(arch);
|
||||
let Some(runtime_dir) = glob::glob(&glob_path(&arch_dir, "Microsoft.VC*.CRT"))?
|
||||
.filter_map(Result::ok)
|
||||
.find(|path| path.is_dir())
|
||||
else {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"failed to find Visual C++ runtime directory for `{arch}` in `{}`",
|
||||
arch_dir.display()
|
||||
)));
|
||||
};
|
||||
|
||||
Ok(runtime_dir)
|
||||
}
|
||||
|
||||
fn latest_vc_redist_version_dir(redist_dir: &Path) -> crate::Result<Option<PathBuf>> {
|
||||
let dir = fs::read_dir(redist_dir)?
|
||||
.flatten()
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.is_dir())
|
||||
.filter_map(|path| {
|
||||
let version = path
|
||||
.file_name()?
|
||||
.to_str()?
|
||||
.parse::<semver::Version>()
|
||||
.ok()?;
|
||||
Some((version, path))
|
||||
})
|
||||
.max_by(|(a, _), (b, _)| a.cmp(b))
|
||||
.map(|(_, path)| path);
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
/// Builds a glob pattern from a literal base path and an unescaped glob suffix.
|
||||
///
|
||||
/// The base path is escaped so Visual Studio paths containing glob metacharacters are treated as
|
||||
/// literal directories, while `pattern` remains active glob syntax.
|
||||
fn glob_path(path: &Path, pattern: &str) -> String {
|
||||
PathBuf::from(glob::Pattern::escape(&path.to_string_lossy()))
|
||||
.join(pattern)
|
||||
.to_string_lossy()
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
/// Returns the bundled `vswhere.exe` path.
|
||||
///
|
||||
/// The executable is written to a temporary file so callers do not depend on a system-installed
|
||||
/// `vswhere.exe`.
|
||||
pub fn vswhere_path() -> Option<PathBuf> {
|
||||
let mut vswhere = std::env::temp_dir();
|
||||
vswhere.push("vswhere.exe");
|
||||
|
||||
if !vswhere.exists() {
|
||||
let mut file = std::fs::File::create(&vswhere).ok()?;
|
||||
file.write_all(VSWHERE).ok()?;
|
||||
}
|
||||
|
||||
Some(vswhere)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn processor_architecture<'a>() -> Option<&'a str> {
|
||||
use windows_sys::Win32::System::SystemInformation::{
|
||||
GetNativeSystemInfo, PROCESSOR_ARCHITECTURE_AMD64, PROCESSOR_ARCHITECTURE_ARM,
|
||||
PROCESSOR_ARCHITECTURE_ARM64, PROCESSOR_ARCHITECTURE_INTEL, SYSTEM_INFO,
|
||||
};
|
||||
|
||||
let mut system_info: SYSTEM_INFO = unsafe { std::mem::zeroed() };
|
||||
unsafe { GetNativeSystemInfo(&mut system_info) };
|
||||
match unsafe { system_info.Anonymous.Anonymous.wProcessorArchitecture } {
|
||||
PROCESSOR_ARCHITECTURE_INTEL => Some("x86"),
|
||||
PROCESSOR_ARCHITECTURE_AMD64 => Some("x64"),
|
||||
PROCESSOR_ARCHITECTURE_ARM => Some("arm"),
|
||||
PROCESSOR_ARCHITECTURE_ARM64 => Some("arm64"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
277
crates/tauri-bundler/src/error.rs
Normal file
277
crates/tauri-bundler/src/error.rs
Normal file
@@ -0,0 +1,277 @@
|
||||
// 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
|
||||
|
||||
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 {
|
||||
// TODO: Change this and the `Context` trait to `Box<dyn std::error::Error + Send + Sync + 'static>` in v3
|
||||
/// 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.
|
||||
#[error("`{0}`")]
|
||||
IoError(#[from] io::Error),
|
||||
/// Image error.
|
||||
#[error("`{0}`")]
|
||||
ImageError(#[from] image::ImageError),
|
||||
/// Error walking directory.
|
||||
#[error("`{0}`")]
|
||||
WalkdirError(#[from] walkdir::Error),
|
||||
/// Strip prefix error.
|
||||
#[error("`{0}`")]
|
||||
StripError(#[from] path::StripPrefixError),
|
||||
/// Number parse error.
|
||||
#[error("`{0}`")]
|
||||
ConvertError(#[from] num::TryFromIntError),
|
||||
/// Zip error.
|
||||
#[error("`{0}`")]
|
||||
ZipError(#[from] zip::result::ZipError),
|
||||
/// Hex error.
|
||||
#[error("`{0}`")]
|
||||
HexError(#[from] hex::FromHexError),
|
||||
/// Handlebars template error.
|
||||
#[error("`{0}`")]
|
||||
HandleBarsError(#[from] handlebars::RenderError),
|
||||
/// JSON error.
|
||||
#[error("`{0}`")]
|
||||
JsonError(#[from] serde_json::error::Error),
|
||||
/// Regex error.
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
#[error("`{0}`")]
|
||||
RegexError(#[from] regex::Error),
|
||||
/// Failed to perform HTTP request.
|
||||
#[error("`{0}`")]
|
||||
HttpError(#[from] Box<ureq::Error>),
|
||||
/// Invalid glob pattern.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
#[error("{0}")]
|
||||
GlobPattern(#[from] glob::PatternError),
|
||||
/// Failed to use glob pattern.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))]
|
||||
#[error("`{0}`")]
|
||||
Glob(#[from] glob::GlobError),
|
||||
/// Failed to parse the URL
|
||||
#[error("`{0}`")]
|
||||
UrlParse(#[from] url::ParseError),
|
||||
/// Failed to validate downloaded file hash.
|
||||
#[error("hash mismatch of downloaded file")]
|
||||
HashError,
|
||||
/// 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),
|
||||
/// Couldn't find icons.
|
||||
#[error("Could not find Icon paths. Please make sure they exist in the tauri config JSON file")]
|
||||
IconPathError,
|
||||
/// Couldn't find background file.
|
||||
#[error(
|
||||
"Could not find background file. Make sure it exists in the tauri config JSON file and extension is png/jpg/gif"
|
||||
)]
|
||||
BackgroundPathError,
|
||||
/// Error on path util operation.
|
||||
#[error("Path Error:`{0}`")]
|
||||
PathUtilError(String),
|
||||
/// Error on shell script.
|
||||
#[error("Shell Scripting Error:`{0}`")]
|
||||
ShellScriptError(String),
|
||||
/// Generic error.
|
||||
#[error("`{0}`")]
|
||||
GenericError(String),
|
||||
/// No bundled project found for the updater.
|
||||
#[error("Unable to find a bundled project for the updater")]
|
||||
UnableToFindProject,
|
||||
/// String is not UTF-8.
|
||||
#[error("string is not UTF-8")]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
/// Windows SignTool not found.
|
||||
#[error("SignTool not found")]
|
||||
SignToolNotFound,
|
||||
/// Failed to open Windows registry.
|
||||
#[error("failed to open registry {0}")]
|
||||
OpenRegistry(String),
|
||||
/// Failed to get registry value.
|
||||
#[error("failed to get {0} value on registry")]
|
||||
GetRegistryValue(String),
|
||||
/// Failed to enumerate registry keys.
|
||||
#[error("failed to enumerate registry keys")]
|
||||
FailedToEnumerateRegKeys,
|
||||
/// Unsupported OS bitness.
|
||||
#[error("unsupported OS bitness")]
|
||||
UnsupportedBitness,
|
||||
/// Failed to sign application.
|
||||
#[error("failed to sign app: {0}")]
|
||||
Sign(String),
|
||||
/// time error.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error("`{0}`")]
|
||||
TimeError(#[from] time::error::Error),
|
||||
/// Plist error.
|
||||
#[cfg(target_os = "macos")]
|
||||
#[error(transparent)]
|
||||
Plist(#[from] plist::Error),
|
||||
/// Rpm 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;
|
||||
33
crates/tauri-bundler/src/lib.rs
Normal file
33
crates/tauri-bundler/src/lib.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
// 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
|
||||
|
||||
//! The Tauri bundler is a tool that generates installers or app bundles for executables.
|
||||
//! It supports auto updating through [tauri](https://docs.rs/tauri).
|
||||
//!
|
||||
//! ## Stability
|
||||
//!
|
||||
//! As it's intended to be used primarily in `tauri-cli`, the public API does not strictly adhere to SemVer.
|
||||
//! For example, minor releases may add new struct fields and change or remove Error enum variants.
|
||||
//!
|
||||
//! ## Platform support
|
||||
//! - macOS
|
||||
//! - DMG and App bundles
|
||||
//! - Linux
|
||||
//! - Appimage, Debian and RPM packages
|
||||
//! - Windows
|
||||
//! - MSI using WiX
|
||||
|
||||
#![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"
|
||||
)]
|
||||
#![warn(missing_docs, rust_2018_idioms)]
|
||||
|
||||
/// The bundle API.
|
||||
pub mod bundle;
|
||||
mod error;
|
||||
mod utils;
|
||||
pub use bundle::*;
|
||||
pub use error::{Error, Result};
|
||||
240
crates/tauri-bundler/src/utils/fs_utils.rs
Normal file
240
crates/tauri-bundler/src/utils/fs_utils.rs
Normal file
@@ -0,0 +1,240 @@
|
||||
// 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
|
||||
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, BufWriter},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Creates a new file at the given path, creating any parent directories as
|
||||
/// needed.
|
||||
pub fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
let file = File::create(path)?;
|
||||
Ok(BufWriter::new(file))
|
||||
}
|
||||
|
||||
/// Creates the given directory path,
|
||||
/// erasing it first if specified.
|
||||
#[allow(dead_code)]
|
||||
pub fn create_dir(path: &Path, erase: bool) -> crate::Result<()> {
|
||||
if erase && path.exists() {
|
||||
remove_dir_all(path)?;
|
||||
}
|
||||
Ok(fs::create_dir(path)?)
|
||||
}
|
||||
|
||||
/// Creates all of the directories of the specified path,
|
||||
/// erasing it first if specified.
|
||||
#[allow(dead_code)]
|
||||
pub fn create_dir_all(path: &Path, erase: bool) -> crate::Result<()> {
|
||||
if erase && path.exists() {
|
||||
remove_dir_all(path)?;
|
||||
}
|
||||
Ok(fs::create_dir_all(path)?)
|
||||
}
|
||||
|
||||
/// Removes the directory and its contents if it exists.
|
||||
#[allow(dead_code)]
|
||||
pub fn remove_dir_all(path: &Path) -> crate::Result<()> {
|
||||
if path.exists() {
|
||||
Ok(fs::remove_dir_all(path)?)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a symbolic link to a directory.
|
||||
#[cfg(unix)]
|
||||
#[allow(dead_code)]
|
||||
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
/// Makes a symbolic link to a directory.
|
||||
#[cfg(windows)]
|
||||
fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::windows::fs::symlink_dir(src, dst)
|
||||
}
|
||||
|
||||
/// Makes a symbolic link to a file.
|
||||
#[cfg(unix)]
|
||||
#[allow(dead_code)]
|
||||
fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::unix::fs::symlink(src, dst)
|
||||
}
|
||||
|
||||
/// Makes a symbolic link to a file.
|
||||
#[cfg(windows)]
|
||||
fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
|
||||
std::os::windows::fs::symlink_file(src, dst)
|
||||
}
|
||||
|
||||
/// Copies a regular file from one path to another, creating any parent
|
||||
/// directories of the destination path as necessary. Fails if the source path
|
||||
/// is a directory or doesn't exist.
|
||||
pub fn copy_file(from: &Path, to: &Path) -> crate::Result<()> {
|
||||
if !from.exists() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{from:?} does not exist"
|
||||
)));
|
||||
}
|
||||
if !from.is_file() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{from:?} is not a file"
|
||||
)));
|
||||
}
|
||||
let dest_dir = to.parent().expect("No data in parent");
|
||||
fs::create_dir_all(dest_dir)?;
|
||||
fs::copy(from, to)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Recursively copies a directory file from one path to another, creating any
|
||||
/// parent directories of the destination path as necessary. Fails if the
|
||||
/// source path is not a directory or doesn't exist, or if the destination path
|
||||
/// already exists.
|
||||
#[allow(dead_code)]
|
||||
pub fn copy_dir(from: &Path, to: &Path) -> crate::Result<()> {
|
||||
if !from.exists() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{from:?} does not exist"
|
||||
)));
|
||||
}
|
||||
if !from.is_dir() {
|
||||
return Err(crate::Error::GenericError(format!(
|
||||
"{from:?} is not a Directory"
|
||||
)));
|
||||
}
|
||||
let parent = to.parent().expect("No data in parent");
|
||||
fs::create_dir_all(parent)?;
|
||||
for entry in walkdir::WalkDir::new(from) {
|
||||
let entry = entry?;
|
||||
debug_assert!(entry.path().starts_with(from));
|
||||
let rel_path = entry.path().strip_prefix(from)?;
|
||||
let dest_path = to.join(rel_path);
|
||||
if entry.file_type().is_symlink() {
|
||||
let target = fs::read_link(entry.path())?;
|
||||
if entry.path().is_dir() {
|
||||
symlink_dir(&target, &dest_path)?;
|
||||
} else {
|
||||
symlink_file(&target, &dest_path)?;
|
||||
}
|
||||
} else if entry.file_type().is_dir() {
|
||||
fs::create_dir_all(dest_path)?;
|
||||
} else {
|
||||
fs::copy(entry.path(), dest_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Copies user-defined files specified in the configuration file to the package.
|
||||
///
|
||||
/// The configuration object maps the path in the package to the path of the file on the filesystem,
|
||||
/// relative to the tauri.conf.json file.
|
||||
///
|
||||
/// Expects a HashMap of PathBuf entries, representing destination and source paths,
|
||||
/// and also a path of a directory. The files will be stored with respect to this directory.
|
||||
#[cfg(any(
|
||||
target_os = "linux",
|
||||
target_os = "dragonfly",
|
||||
target_os = "freebsd",
|
||||
target_os = "netbsd",
|
||||
target_os = "openbsd"
|
||||
))]
|
||||
pub fn copy_custom_files(
|
||||
files_map: &std::collections::HashMap<std::path::PathBuf, std::path::PathBuf>,
|
||||
data_dir: &Path,
|
||||
) -> crate::Result<()> {
|
||||
for (pkg_path, path) in files_map.iter() {
|
||||
let pkg_path = if pkg_path.is_absolute() {
|
||||
pkg_path.strip_prefix("/").unwrap()
|
||||
} else {
|
||||
pkg_path
|
||||
};
|
||||
if path.is_file() {
|
||||
copy_file(path, &data_dir.join(pkg_path))?;
|
||||
} else {
|
||||
copy_dir(path, &data_dir.join(pkg_path))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::create_file;
|
||||
use std::io::Write;
|
||||
|
||||
#[test]
|
||||
fn create_file_with_parent_dirs() {
|
||||
let tmp = tempfile::tempdir().expect("Unable to create temp dir");
|
||||
assert!(!tmp.path().join("parent").exists());
|
||||
{
|
||||
let mut file =
|
||||
create_file(&tmp.path().join("parent/file.txt")).expect("Failed to create file");
|
||||
writeln!(file, "Hello, world!").expect("unable to write file");
|
||||
}
|
||||
assert!(tmp.path().join("parent").is_dir());
|
||||
assert!(tmp.path().join("parent/file.txt").is_file());
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
#[test]
|
||||
fn copy_dir_with_symlinks() {
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Create a directory structure that looks like this:
|
||||
// ${TMP}/orig/
|
||||
// sub/
|
||||
// file.txt
|
||||
// link -> sub/file.txt
|
||||
let tmp = tempfile::tempdir().expect("unable to create tempdir");
|
||||
{
|
||||
let mut file =
|
||||
create_file(&tmp.path().join("orig/sub/file.txt")).expect("Unable to create file");
|
||||
writeln!(file, "Hello, world!").expect("Unable to write to file");
|
||||
}
|
||||
super::symlink_file(
|
||||
&PathBuf::from("sub/file.txt"),
|
||||
&tmp.path().join("orig/link"),
|
||||
)
|
||||
.expect("Failed to create symlink");
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("orig/link"))
|
||||
.expect("Failed to read file")
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
// Copy ${TMP}/orig to ${TMP}/parent/copy, and make sure that the
|
||||
// directory structure, file, and symlink got copied correctly.
|
||||
super::copy_dir(&tmp.path().join("orig"), &tmp.path().join("parent/copy"))
|
||||
.expect("Failed to copy dir");
|
||||
assert!(tmp.path().join("parent/copy").is_dir());
|
||||
assert!(tmp.path().join("parent/copy/sub").is_dir());
|
||||
assert!(tmp.path().join("parent/copy/sub/file.txt").is_file());
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("parent/copy/sub/file.txt"))
|
||||
.expect("Failed to read file")
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
assert!(tmp.path().join("parent/copy/link").exists());
|
||||
assert_eq!(
|
||||
std::fs::read_link(tmp.path().join("parent/copy/link")).expect("Failed to read from symlink"),
|
||||
PathBuf::from("sub/file.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
std::fs::read(tmp.path().join("parent/copy/link"))
|
||||
.expect("Failed to read from file")
|
||||
.as_slice(),
|
||||
b"Hello, world!\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
246
crates/tauri-bundler/src/utils/http_utils.rs
Normal file
246
crates/tauri-bundler/src/utils/http_utils.rs
Normal file
@@ -0,0 +1,246 @@
|
||||
// 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
|
||||
|
||||
use std::{
|
||||
fs::{File, create_dir_all},
|
||||
io::{Cursor, Read, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use regex::Regex;
|
||||
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()
|
||||
.and_then(|template| {
|
||||
let re =
|
||||
Regex::new(r"https://github.com/([^/]+)/([^/]+)/releases/download/([^/]+)/(.*)").unwrap();
|
||||
re.captures(github_url).map(|caps| {
|
||||
template
|
||||
.replace("<owner>", &caps[1])
|
||||
.replace("<repo>", &caps[2])
|
||||
.replace("<version>", &caps[3])
|
||||
.replace("<asset>", &caps[4])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_github_mirror_url_from_base(github_url: &str) -> Option<String> {
|
||||
std::env::var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR")
|
||||
.ok()
|
||||
.and_then(|cdn| Url::parse(&cdn).ok())
|
||||
.map(|mut cdn| {
|
||||
cdn.set_path(github_url);
|
||||
cdn.to_string()
|
||||
})
|
||||
}
|
||||
|
||||
fn generate_github_alternative_url(url: &str) -> Option<(ureq::Agent, String)> {
|
||||
if !url.starts_with("https://github.com/") {
|
||||
return None;
|
||||
}
|
||||
|
||||
generate_github_mirror_url_from_template(url)
|
||||
.or_else(|| generate_github_mirror_url_from_base(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((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)]
|
||||
pub fn download(url: &str) -> crate::Result<Vec<u8>> {
|
||||
let (agent, final_url) = create_agent_and_url(url);
|
||||
|
||||
log::info!(action = "Downloading"; "{}", final_url);
|
||||
|
||||
let response = agent.get(&final_url).call().map_err(Box::new)?;
|
||||
let mut bytes = Vec::new();
|
||||
response.into_body().into_reader().read_to_end(&mut bytes)?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum HashAlgorithm {
|
||||
#[cfg(target_os = "windows")]
|
||||
Sha256,
|
||||
Sha1,
|
||||
}
|
||||
|
||||
/// Function used to download a file and checks SHA256 to verify the download.
|
||||
#[allow(dead_code)]
|
||||
pub fn download_and_verify(
|
||||
url: &str,
|
||||
hash: &str,
|
||||
hash_algorithm: HashAlgorithm,
|
||||
) -> crate::Result<Vec<u8>> {
|
||||
let data = download(url)?;
|
||||
log::info!("validating hash");
|
||||
verify_hash(&data, hash, hash_algorithm)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn verify_hash(data: &[u8], hash: &str, hash_algorithm: HashAlgorithm) -> crate::Result<()> {
|
||||
match hash_algorithm {
|
||||
#[cfg(target_os = "windows")]
|
||||
HashAlgorithm::Sha256 => {
|
||||
let hasher = sha2::Sha256::new();
|
||||
verify_data_with_hasher(data, hash, hasher)
|
||||
}
|
||||
HashAlgorithm::Sha1 => {
|
||||
let hasher = sha1::Sha1::new();
|
||||
verify_data_with_hasher(data, hash, hasher)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_data_with_hasher(data: &[u8], hash: &str, mut hasher: impl Digest) -> crate::Result<()> {
|
||||
hasher.update(data);
|
||||
|
||||
let url_hash = hasher.finalize().to_vec();
|
||||
let expected_hash = hex::decode(hash)?;
|
||||
if expected_hash == url_hash {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(crate::Error::HashError)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn verify_file_hash<P: AsRef<Path>>(
|
||||
path: P,
|
||||
hash: &str,
|
||||
hash_algorithm: HashAlgorithm,
|
||||
) -> crate::Result<()> {
|
||||
let data = std::fs::read(path)?;
|
||||
verify_hash(&data, hash, hash_algorithm)
|
||||
}
|
||||
|
||||
/// Extracts the zips from memory into a usable path.
|
||||
#[allow(dead_code)]
|
||||
pub fn extract_zip(data: &[u8], path: &Path) -> crate::Result<()> {
|
||||
let cursor = Cursor::new(data);
|
||||
|
||||
let mut zipa = ZipArchive::new(cursor)?;
|
||||
|
||||
for i in 0..zipa.len() {
|
||||
let mut file = zipa.by_index(i)?;
|
||||
|
||||
if let Some(name) = file.enclosed_name() {
|
||||
let dest_path = path.join(name);
|
||||
if file.is_dir() {
|
||||
create_dir_all(&dest_path)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
let parent = dest_path.parent().expect("Failed to get parent");
|
||||
|
||||
if !parent.exists() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
|
||||
let mut buff: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buff)?;
|
||||
let mut fileout = File::create(dest_path).expect("Failed to open file");
|
||||
|
||||
fileout.write_all(&buff)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::generate_github_mirror_url_from_template;
|
||||
use std::env;
|
||||
|
||||
const GITHUB_ASSET_URL: &str =
|
||||
"https://github.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip";
|
||||
const NON_GITHUB_ASSET_URL: &str = "https://someotherwebsite.com/somefile.zip";
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_generate_mirror_url_no_env_var() {
|
||||
unsafe { env::remove_var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE") };
|
||||
|
||||
assert!(generate_github_mirror_url_from_template(GITHUB_ASSET_URL).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_generate_mirror_url_non_github_url() {
|
||||
unsafe {
|
||||
env::set_var(
|
||||
"TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE",
|
||||
"https://mirror.example.com/<owner>/<repo>/releases/download/<version>/<asset>",
|
||||
)
|
||||
};
|
||||
|
||||
assert!(generate_github_mirror_url_from_template(NON_GITHUB_ASSET_URL).is_none());
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
template: &'static str,
|
||||
expected_url: &'static str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial_test::serial]
|
||||
fn test_generate_mirror_url_correctly() {
|
||||
let test_cases = vec![
|
||||
TestCase {
|
||||
template: "https://mirror.example.com/<owner>/<repo>/releases/download/<version>/<asset>",
|
||||
expected_url: "https://mirror.example.com/wixtoolset/wix3/releases/download/wix3112rtm/wix311-binaries.zip",
|
||||
},
|
||||
TestCase {
|
||||
template: "https://mirror.example.com/<asset>",
|
||||
expected_url: "https://mirror.example.com/wix311-binaries.zip",
|
||||
},
|
||||
];
|
||||
|
||||
for case in test_cases {
|
||||
unsafe { env::set_var("TAURI_BUNDLER_TOOLS_GITHUB_MIRROR_TEMPLATE", case.template) };
|
||||
assert_eq!(
|
||||
generate_github_mirror_url_from_template(GITHUB_ASSET_URL),
|
||||
Some(case.expected_url.to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
141
crates/tauri-bundler/src/utils/mod.rs
Normal file
141
crates/tauri-bundler/src/utils/mod.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
// 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
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
process::{Command, ExitStatus, Output, Stdio},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
pub mod fs_utils;
|
||||
pub mod http_utils;
|
||||
|
||||
/// Returns true if the path has a filename indicating that it is a high-density
|
||||
/// "retina" icon. Specifically, returns true the file stem ends with
|
||||
/// "@2x" (a convention specified by the [Apple developer docs](
|
||||
/// <https://developer.apple.com/library/mac/documentation/GraphicsAnimation/Conceptual/HighResolutionOSX/Optimizing/Optimizing.html>)).
|
||||
#[allow(dead_code)]
|
||||
pub fn is_retina(path: &Path) -> bool {
|
||||
path
|
||||
.file_stem()
|
||||
.and_then(OsStr::to_str)
|
||||
.map(|stem| stem.ends_with("@2x"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub trait CommandExt {
|
||||
// The `pipe` function sets the stdout and stderr to properly
|
||||
// show the command output in the Node.js wrapper.
|
||||
fn piped(&mut self) -> std::io::Result<ExitStatus>;
|
||||
fn output_ok(&mut self) -> crate::Result<Output>;
|
||||
}
|
||||
|
||||
impl CommandExt for Command {
|
||||
fn piped(&mut self) -> std::io::Result<ExitStatus> {
|
||||
self.stdin(os_pipe::dup_stdin()?);
|
||||
self.stdout(os_pipe::dup_stdout()?);
|
||||
self.stderr(os_pipe::dup_stderr()?);
|
||||
let program = self.get_program().to_string_lossy().into_owned();
|
||||
log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
|
||||
self.status()
|
||||
}
|
||||
|
||||
fn output_ok(&mut self) -> crate::Result<Output> {
|
||||
let program = self.get_program().to_string_lossy().into_owned();
|
||||
log::debug!(action = "Running"; "Command `{} {}`", program, self.get_args().map(|arg| arg.to_string_lossy()).fold(String::new(), |acc, arg| format!("{acc} {arg}")));
|
||||
|
||||
self.stdout(Stdio::piped());
|
||||
self.stderr(Stdio::piped());
|
||||
|
||||
let mut child = self.spawn()?;
|
||||
|
||||
let mut stdout = child.stdout.take().map(BufReader::new).unwrap();
|
||||
let stdout_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stdout_lines_ = stdout_lines.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut line = String::new();
|
||||
let mut lines = stdout_lines_.lock().unwrap();
|
||||
loop {
|
||||
line.clear();
|
||||
match stdout.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stdout"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes().to_vec());
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut stderr = child.stderr.take().map(BufReader::new).unwrap();
|
||||
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
|
||||
let stderr_lines_ = stderr_lines.clone();
|
||||
std::thread::spawn(move || {
|
||||
let mut line = String::new();
|
||||
let mut lines = stderr_lines_.lock().unwrap();
|
||||
loop {
|
||||
line.clear();
|
||||
match stderr.read_line(&mut line) {
|
||||
Ok(0) => break,
|
||||
Ok(_) => {
|
||||
log::debug!(action = "stderr"; "{}", line.trim_end());
|
||||
lines.extend(line.as_bytes().to_vec());
|
||||
}
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let status = child.wait()?;
|
||||
let output = Output {
|
||||
status,
|
||||
stdout: std::mem::take(&mut *stdout_lines.lock().unwrap()),
|
||||
stderr: std::mem::take(&mut *stderr_lines.lock().unwrap()),
|
||||
};
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output)
|
||||
} else {
|
||||
Err(crate::Error::GenericError(format!(
|
||||
"failed to run {program}"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use tauri_utils::resources::resource_relpath;
|
||||
|
||||
use super::is_retina;
|
||||
|
||||
#[test]
|
||||
fn retina_icon_paths() {
|
||||
assert!(!is_retina(Path::new("data/icons/512x512.png")));
|
||||
assert!(is_retina(Path::new("data/icons/512x512@2x.png")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resource_relative_paths() {
|
||||
assert_eq!(
|
||||
resource_relpath(Path::new("./data/images/button.png")),
|
||||
PathBuf::from("data/images/button.png")
|
||||
);
|
||||
assert_eq!(
|
||||
resource_relpath(Path::new("../../images/wheel.png")),
|
||||
PathBuf::from("_up_/_up_/images/wheel.png")
|
||||
);
|
||||
assert_eq!(
|
||||
resource_relpath(Path::new("/home/ferris/crab.png")),
|
||||
PathBuf::from("_root_/home/ferris/crab.png")
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user