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

This commit is contained in:
upstream-mirror
2026-09-23 17:00:42 +05:00
commit feda81bf2f
1178 changed files with 156404 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
cmp::Ordering,
env::current_dir,
ffi::OsStr,
path::{Path, PathBuf},
sync::OnceLock,
};
use ignore::WalkBuilder;
use tauri_utils::{
config::parse::{ConfigFormat, folder_has_configuration_file, is_configuration_file},
platform::Target,
};
const TAURI_GITIGNORE: &[u8] = include_bytes!("../../tauri.gitignore");
// path to the Tauri app (Rust crate) directory, usually `<project>/src-tauri/`
const ENV_TAURI_APP_PATH: &str = "TAURI_APP_PATH";
// path to the frontend app directory, usually `<project>/`
const ENV_TAURI_FRONTEND_PATH: &str = "TAURI_FRONTEND_PATH";
pub struct Dirs {
pub tauri: &'static Path,
pub frontend: &'static Path,
}
static FRONTEND_DIR: OnceLock<PathBuf> = OnceLock::new();
static TAURI_DIR: OnceLock<PathBuf> = OnceLock::new();
pub fn walk_builder(path: &Path) -> WalkBuilder {
let mut default_gitignore = std::env::temp_dir();
default_gitignore.push(".gitignore");
if !default_gitignore.exists() {
if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) {
use std::io::Write;
let _ = file.write_all(TAURI_GITIGNORE);
}
}
let mut builder = WalkBuilder::new(path);
builder.add_custom_ignore_filename(".taurignore");
builder.git_global(false);
builder.parents(false);
let _ = builder.add_ignore(default_gitignore);
builder
}
fn lookup<F: Fn(&PathBuf) -> bool>(dir: &Path, checker: F) -> Option<PathBuf> {
let mut builder = walk_builder(dir);
builder
.require_git(false)
.ignore(false)
.max_depth(Some(
std::env::var("TAURI_CLI_CONFIG_DEPTH")
.map(|d| {
d.parse()
.expect("`TAURI_CLI_CONFIG_DEPTH` environment variable must be a positive integer")
})
.unwrap_or(3),
))
.sort_by_file_path(|a, _| {
if a.extension().is_some() {
Ordering::Less
} else {
Ordering::Greater
}
});
for entry in builder.build().flatten() {
let path = dir.join(entry.path());
if checker(&path) {
return Some(path);
}
}
None
}
fn env_tauri_app_path() -> Option<PathBuf> {
let p = PathBuf::from(std::env::var_os(ENV_TAURI_APP_PATH)?);
dunce::canonicalize(p).ok()
}
fn env_tauri_frontend_path() -> Option<PathBuf> {
let p = PathBuf::from(std::env::var_os(ENV_TAURI_FRONTEND_PATH)?);
dunce::canonicalize(p).ok()
}
pub fn resolve_tauri_dir() -> Option<PathBuf> {
let src_dir = env_tauri_app_path().or_else(|| current_dir().ok())?;
for standard_tauri_path in [src_dir.clone(), src_dir.join("src-tauri")] {
if standard_tauri_path
.join(ConfigFormat::Json.into_file_name())
.exists()
|| standard_tauri_path
.join(ConfigFormat::Json5.into_file_name())
.exists()
|| standard_tauri_path
.join(ConfigFormat::Toml.into_file_name())
.exists()
{
log::debug!(
"Found Tauri project inside {} on early lookup",
standard_tauri_path.display()
);
return Some(standard_tauri_path);
}
}
log::debug!("resolving Tauri directory from {}", src_dir.display());
lookup(&src_dir, |path| {
folder_has_configuration_file(Target::Linux, path) || is_configuration_file(Target::Linux, path)
})
.map(|p| {
if p.is_dir() {
log::debug!("Found Tauri project directory {}", p.display());
p
} else {
log::debug!("Found Tauri project configuration file {}", p.display());
p.parent().unwrap().to_path_buf()
}
})
}
pub fn resolve_dirs() -> Dirs {
let tauri = TAURI_DIR.get_or_init(|| resolve_tauri_dir().unwrap_or_else(|| {
let env_var_name = env_tauri_app_path().is_some().then(|| format!("`{ENV_TAURI_APP_PATH}`"));
panic!("Couldn't recognize the {} folder as a Tauri project. It must contain a `{}`, `{}` or `{}` file in any subfolder.",
env_var_name.as_deref().unwrap_or("current"),
ConfigFormat::Json.into_file_name(),
ConfigFormat::Json5.into_file_name(),
ConfigFormat::Toml.into_file_name()
)
}));
let frontend = FRONTEND_DIR.get_or_init(|| {
resolve_frontend_dir().unwrap_or_else(|| tauri.parent().unwrap().to_path_buf())
});
Dirs { tauri, frontend }
}
pub fn resolve_frontend_dir() -> Option<PathBuf> {
let frontend_dir =
env_tauri_frontend_path().unwrap_or_else(|| current_dir().expect("failed to read cwd"));
if frontend_dir.join("package.json").exists() {
return Some(frontend_dir);
}
log::debug!(
"resolving frontend directory from {}",
frontend_dir.display()
);
lookup(&frontend_dir, |path| {
if let Some(file_name) = path.file_name() {
file_name == OsStr::new("package.json")
} else {
false
}
})
.map(|p| p.parent().unwrap().to_path_buf())
}

View File

@@ -0,0 +1,99 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::process::Command;
use crate::Error;
#[derive(Debug, Default, Clone, Copy)]
pub struct CargoInstallOptions<'a> {
pub name: &'a str,
pub version: Option<&'a str>,
pub rev: Option<&'a str>,
pub tag: Option<&'a str>,
pub branch: Option<&'a str>,
pub cwd: Option<&'a std::path::Path>,
pub target: Option<&'a str>,
}
pub fn install_one(options: CargoInstallOptions) -> crate::Result<()> {
let mut cargo = Command::new("cargo");
cargo.arg("add");
if let Some(version) = options.version {
cargo.arg(format!("{}@{}", options.name, version));
} else {
cargo.arg(options.name);
if options.tag.is_some() || options.rev.is_some() || options.branch.is_some() {
cargo.args(["--git", "https://github.com/tauri-apps/plugins-workspace"]);
}
match (options.tag, options.rev, options.branch) {
(Some(tag), None, None) => {
cargo.args(["--tag", tag]);
}
(None, Some(rev), None) => {
cargo.args(["--rev", rev]);
}
(None, None, Some(branch)) => {
cargo.args(["--branch", branch]);
}
(None, None, None) => {}
_ => crate::error::bail!("Only one of --tag, --rev and --branch can be specified"),
};
}
if let Some(target) = options.target {
cargo.args(["--target", target]);
}
if let Some(cwd) = options.cwd {
cargo.current_dir(cwd);
}
log::info!("Installing Cargo dependency \"{}\"...", options.name);
let status = cargo.status().map_err(|error| Error::CommandFailed {
command: "cargo add".to_string(),
error,
})?;
if !status.success() {
crate::error::bail!("Failed to install Cargo dependency");
}
Ok(())
}
#[derive(Debug, Default, Clone, Copy)]
pub struct CargoUninstallOptions<'a> {
pub name: &'a str,
pub cwd: Option<&'a std::path::Path>,
pub target: Option<&'a str>,
}
pub fn uninstall_one(options: CargoUninstallOptions) -> crate::Result<()> {
let mut cargo = Command::new("cargo");
cargo.arg("remove");
cargo.arg(options.name);
if let Some(target) = options.target {
cargo.args(["--target", target]);
}
if let Some(cwd) = options.cwd {
cargo.current_dir(cwd);
}
log::info!("Uninstalling Cargo dependency \"{}\"...", options.name);
let status = cargo.status().map_err(|error| Error::CommandFailed {
command: "cargo remove".to_string(),
error,
})?;
if !status.success() {
crate::error::bail!("Failed to remove Cargo dependency");
}
Ok(())
}

View File

@@ -0,0 +1,209 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use std::{
collections::HashMap,
fs,
path::{Path, PathBuf},
};
use crate::interface::rust::get_workspace_dir;
#[derive(Clone, Deserialize)]
pub struct CargoLockPackage {
pub name: String,
pub version: String,
pub source: Option<String>,
}
#[derive(Deserialize)]
pub struct CargoLock {
pub package: Vec<CargoLockPackage>,
}
#[derive(Clone, Deserialize)]
pub struct CargoManifestDependencyPackage {
pub version: Option<String>,
pub git: Option<String>,
pub branch: Option<String>,
pub rev: Option<String>,
pub path: Option<PathBuf>,
}
#[derive(Clone, Deserialize)]
#[serde(untagged)]
pub enum CargoManifestDependency {
Version(String),
Package(CargoManifestDependencyPackage),
}
#[derive(Deserialize)]
pub struct CargoManifestPackage {
pub version: String,
}
#[derive(Deserialize)]
pub struct CargoManifest {
pub package: CargoManifestPackage,
pub dependencies: HashMap<String, CargoManifestDependency>,
}
pub fn cargo_manifest_and_lock(tauri_dir: &Path) -> (Option<CargoManifest>, Option<CargoLock>) {
let manifest: Option<CargoManifest> = fs::read_to_string(tauri_dir.join("Cargo.toml"))
.ok()
.and_then(|manifest_contents| toml::from_str(&manifest_contents).ok());
let lock: Option<CargoLock> = get_workspace_dir(tauri_dir)
.ok()
.and_then(|p| fs::read_to_string(p.join("Cargo.lock")).ok())
.and_then(|s| toml::from_str(&s).ok());
(manifest, lock)
}
#[derive(Default)]
pub struct CrateVersion {
pub version: Option<String>,
pub git: Option<String>,
pub git_branch: Option<String>,
pub git_rev: Option<String>,
pub path: Option<PathBuf>,
pub lock_version: Option<String>,
}
impl CrateVersion {
pub fn has_version(&self) -> bool {
self.version.is_some() || self.git.is_some() || self.path.is_some()
}
}
impl std::fmt::Display for CrateVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(g) = &self.git {
if let Some(version) = &self.version {
write!(f, "{g} ({version})")?;
} else {
write!(f, "git:{g}")?;
if let Some(branch) = &self.git_branch {
write!(f, "&branch={branch}")?;
} else if let Some(rev) = &self.git_rev {
write!(f, "#rev={rev}")?;
}
}
} else if let Some(p) = &self.path {
write!(f, "path:{}", p.display())?;
if let Some(version) = &self.version {
write!(f, " ({version})")?;
}
} else if let Some(version) = &self.version {
write!(f, "{version}")?;
} else {
return write!(f, "No version detected");
}
if let Some(lock_version) = &self.lock_version {
write!(f, " ({lock_version})")?;
}
Ok(())
}
}
// Reference: https://github.com/rust-lang/crates.io/blob/98c83c8231cbcd15d6b8f06d80a00ad462f71585/src/views.rs#L274
#[derive(serde::Deserialize)]
struct CrateMetadata {
/// The "default" version of this crate.
///
/// This version will be displayed by default on the crate's page.
pub default_version: Option<String>,
}
// Reference: https://github.com/rust-lang/crates.io/blob/98c83c8231cbcd15d6b8f06d80a00ad462f71585/src/controllers/krate/metadata.rs#L44
#[derive(serde::Deserialize)]
struct CrateIoGetResponse {
/// The crate metadata.
#[serde(rename = "crate")]
krate: CrateMetadata,
}
pub fn crate_latest_version(name: &str) -> Option<String> {
// Reference: https://github.com/rust-lang/crates.io/blob/98c83c8231cbcd15d6b8f06d80a00ad462f71585/src/controllers/krate/metadata.rs#L88
let url = format!("https://crates.io/api/v1/crates/{name}?include");
let mut response = super::http::get(&url).ok()?;
let metadata: CrateIoGetResponse =
serde_json::from_reader(response.body_mut().as_reader()).unwrap();
metadata.krate.default_version
}
pub fn crate_version(
tauri_dir: &Path,
manifest: Option<&CargoManifest>,
lock: Option<&CargoLock>,
name: &str,
) -> CrateVersion {
let mut version = CrateVersion::default();
let crate_lock_packages: Vec<CargoLockPackage> = lock
.as_ref()
.map(|lock| {
lock
.package
.iter()
.filter(|p| p.name == name)
.cloned()
.collect()
})
.unwrap_or_default();
if crate_lock_packages.len() == 1 {
let crate_lock_package = crate_lock_packages.first().unwrap();
if let Some(s) = crate_lock_package
.source
.as_ref()
.filter(|s| s.starts_with("git"))
{
version.git = Some(s.clone());
}
version.version = Some(crate_lock_package.version.clone());
} else {
if let Some(dep) = manifest.and_then(|m| m.dependencies.get(name).cloned()) {
match dep {
CargoManifestDependency::Version(v) => version.version = Some(v),
CargoManifestDependency::Package(p) => {
if let Some(v) = p.version {
version.version = Some(v);
} else if let Some(p) = p.path {
let manifest_path = tauri_dir.join(&p).join("Cargo.toml");
let v = fs::read_to_string(manifest_path)
.ok()
.and_then(|m| toml::from_str::<CargoManifest>(&m).ok())
.map(|m| m.package.version);
version.version = v;
version.path = Some(p);
} else if let Some(g) = p.git {
version.git = Some(g);
version.git_branch = p.branch;
version.git_rev = p.rev;
}
}
}
}
if lock.is_some() && crate_lock_packages.is_empty() {
let lock_version = crate_lock_packages
.iter()
.map(|p| p.version.clone())
.collect::<Vec<String>>()
.join(", ");
if !lock_version.is_empty() {
version.lock_version = Some(lock_version);
}
}
}
version
}

View File

@@ -0,0 +1,362 @@
// Copyright 2019-2025 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use itertools::Itertools;
use json_patch::merge;
use serde_json::Value as JsonValue;
use tauri_utils::acl::REMOVE_UNUSED_COMMANDS_ENV_VAR;
pub use tauri_utils::{config::*, platform::Target};
use std::{
collections::HashMap,
env::{current_dir, set_current_dir, set_var},
ffi::{OsStr, OsString},
path::Path,
process::exit,
sync::LazyLock,
};
use crate::error::Context;
pub const MERGE_CONFIG_EXTENSION_NAME: &str = "--config";
pub struct ConfigMetadata {
/// The current target.
target: Target,
original_identifier: Option<String>,
/// The actual configuration, merged with any extension.
inner: Config,
/// The config extensions (platform-specific config files or the config CLI argument).
/// Maps the extension name to its value.
extensions: HashMap<OsString, JsonValue>,
}
impl std::ops::Deref for ConfigMetadata {
type Target = Config;
#[inline(always)]
fn deref(&self) -> &Config {
&self.inner
}
}
impl ConfigMetadata {
/// The original bundle identifier from the config file.
/// This does not take any extensions into account.
pub fn original_identifier(&self) -> Option<&str> {
self.original_identifier.as_deref()
}
/// Checks which config is overwriting the bundle identifier.
pub fn find_bundle_identifier_overwriter(&self) -> Option<OsString> {
for (ext, config) in &self.extensions {
if let Some(identifier) = config
.as_object()
.and_then(|bundle_config| bundle_config.get("identifier")?.as_str())
{
if identifier == self.inner.identifier {
return Some(ext.clone());
}
}
}
None
}
}
pub fn wix_settings(config: WixConfig) -> tauri_bundler::WixSettings {
tauri_bundler::WixSettings {
version: config.version,
upgrade_code: config.upgrade_code,
fips_compliant: std::env::var_os("TAURI_BUNDLER_WIX_FIPS_COMPLIANT")
.map(|v| v == "true")
.unwrap_or(config.fips_compliant),
language: tauri_bundler::WixLanguage(match config.language {
WixLanguage::One(lang) => vec![(lang, Default::default())],
WixLanguage::List(languages) => languages
.into_iter()
.map(|lang| (lang, Default::default()))
.collect(),
WixLanguage::Localized(languages) => languages
.into_iter()
.map(|(lang, config)| {
(
lang,
tauri_bundler::WixLanguageConfig {
locale_path: config.locale_path.map(Into::into),
},
)
})
.collect(),
}),
template: config.template,
fragment_paths: config.fragment_paths,
component_group_refs: config.component_group_refs,
component_refs: config.component_refs,
feature_group_refs: config.feature_group_refs,
feature_refs: config.feature_refs,
merge_refs: config.merge_refs,
enable_elevated_update_task: config.enable_elevated_update_task,
banner_path: config.banner_path,
dialog_image_path: config.dialog_image_path,
}
}
pub fn nsis_settings(config: NsisConfig) -> tauri_bundler::NsisSettings {
tauri_bundler::NsisSettings {
template: config.template,
header_image: config.header_image,
sidebar_image: config.sidebar_image,
installer_icon: config.installer_icon,
uninstaller_icon: config.uninstaller_icon,
uninstaller_header_image: config.uninstaller_header_image,
install_mode: config.install_mode,
languages: config.languages,
custom_language_files: config.custom_language_files,
display_language_selector: config.display_language_selector,
compression: config.compression,
start_menu_folder: config.start_menu_folder,
installer_hooks: config.installer_hooks,
#[allow(deprecated)]
minimum_webview2_version: config.minimum_webview2_version,
}
}
pub fn custom_sign_settings(
config: CustomSignCommandConfig,
) -> tauri_bundler::CustomSignCommandSettings {
match config {
CustomSignCommandConfig::Command(command) => {
let mut tokens = command.split(' ');
tauri_bundler::CustomSignCommandSettings {
cmd: tokens.next().unwrap().to_string(), // split always has at least one element
args: tokens.map(String::from).collect(),
}
}
CustomSignCommandConfig::CommandWithOptions { cmd, args } => {
tauri_bundler::CustomSignCommandSettings { cmd, args }
}
}
}
static CONFIG_SCHEMA_VALIDATOR: LazyLock<jsonschema::Validator> = LazyLock::new(|| {
let schema: JsonValue = serde_json::from_str(include_str!("../../config.schema.json"))
.expect("Failed to parse config schema bundled in the tauri-cli");
jsonschema::validator_for(&schema).expect("Config schema bundled in the tauri-cli is invalid")
});
fn load_config(
merge_configs: &[&serde_json::Value],
reload: bool,
target: Target,
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
let (mut config, config_path) =
tauri_utils::config::parse::parse_value(target, tauri_dir.join("tauri.conf.json"))
.context("failed to parse config")?;
let config_file_name = config_path.file_name().unwrap();
let mut extensions = HashMap::new();
let original_identifier = config
.as_object()
.and_then(|config| config.get("identifier")?.as_str())
.map(ToString::to_string);
if let Some((platform_config, config_path)) =
tauri_utils::config::parse::read_platform(target, tauri_dir)
.context("failed to parse platform config")?
{
merge(&mut config, &platform_config);
extensions.insert(config_path.file_name().unwrap().into(), platform_config);
}
if !merge_configs.is_empty() {
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
let merge_config_str = serde_json::to_string(&merge_config).unwrap();
unsafe { set_var("TAURI_CONFIG", merge_config_str) };
merge(&mut config, &merge_config);
extensions.insert(MERGE_CONFIG_EXTENSION_NAME.into(), merge_config);
}
if config_path.extension() == Some(OsStr::new("json"))
|| config_path.extension() == Some(OsStr::new("json5"))
{
let mut errors = CONFIG_SCHEMA_VALIDATOR.iter_errors(&config).peekable();
if errors.peek().is_some() {
for error in errors {
let path = error.instance_path().into_iter().join(" > ");
if path.is_empty() {
log::error!("`{config_file_name:?}` error: {error}");
} else {
log::error!("`{config_file_name:?}` error on `{path}`: {error}");
}
}
if !reload {
exit(1);
}
}
}
// the `Config` deserializer for `package > version` can resolve the version from a path relative to the config path
// so we actually need to change the current working directory here
let current_dir = current_dir().context("failed to resolve current directory")?;
set_current_dir(config_path.parent().unwrap()).context("failed to set current directory")?;
let config: Config = serde_json::from_value(config).context("failed to parse config")?;
// revert to previous working directory
set_current_dir(current_dir).context("failed to set current directory")?;
for (plugin, conf) in &config.plugins.0 {
unsafe {
set_var(
format!(
"TAURI_{}_PLUGIN_CONFIG",
plugin.to_uppercase().replace('-', "_")
),
serde_json::to_string(&conf).context("failed to serialize config")?,
)
};
}
if config.build.remove_unused_commands {
unsafe { std::env::set_var(REMOVE_UNUSED_COMMANDS_ENV_VAR, tauri_dir) };
}
Ok(ConfigMetadata {
target,
original_identifier,
inner: config,
extensions,
})
}
pub fn get_config(
target: Target,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<ConfigMetadata> {
load_config(merge_configs, false, target, tauri_dir)
}
pub fn reload_config(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
tauri_dir: &Path,
) -> crate::Result<()> {
let target = config.target;
*config = load_config(merge_configs, true, target, tauri_dir)?;
Ok(())
}
/// merges the loaded config with the given value
pub fn merge_config_with(
config: &mut ConfigMetadata,
merge_configs: &[&serde_json::Value],
) -> crate::Result<()> {
if merge_configs.is_empty() {
return Ok(());
}
let mut merge_config = serde_json::Value::Object(Default::default());
for conf in merge_configs {
merge_patches(&mut merge_config, conf);
}
let merge_config_str = serde_json::to_string(&merge_config).unwrap();
unsafe { set_var("TAURI_CONFIG", merge_config_str) };
let mut value =
serde_json::to_value(config.inner.clone()).context("failed to serialize config")?;
merge(&mut value, &merge_config);
config.inner = serde_json::from_value(value).context("failed to parse config")?;
Ok(())
}
/// Same as [`json_patch::merge`] but doesn't delete the key when the patch's value is `null`
fn merge_patches(doc: &mut serde_json::Value, patch: &serde_json::Value) {
use serde_json::{Map, Value};
if !patch.is_object() {
*doc = patch.clone();
return;
}
if !doc.is_object() {
*doc = Value::Object(Map::new());
}
let map = doc.as_object_mut().unwrap();
for (key, value) in patch.as_object().unwrap() {
merge_patches(map.entry(key.as_str()).or_insert(Value::Null), value);
}
}
#[cfg(test)]
mod tests {
#[test]
fn merge_patches() {
let mut json = serde_json::Value::Object(Default::default());
super::merge_patches(
&mut json,
&serde_json::json!({
"app": {
"withGlobalTauri": true,
"windows": []
},
"plugins": {
"test": "tauri"
},
"build": {
"devUrl": "http://localhost:8080"
}
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"app": { "withGlobalTauri": null }
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"app": { "windows": null }
}),
);
super::merge_patches(
&mut json,
&serde_json::json!({
"plugins": { "updater": {
"endpoints": ["https://tauri.app"]
} }
}),
);
assert_eq!(
json,
serde_json::json!({
"app": {
"withGlobalTauri": null,
"windows": null
},
"plugins": {
"test": "tauri",
"updater": {
"endpoints": ["https://tauri.app"]
}
},
"build": {
"devUrl": "http://localhost:8080"
}
})
)
}
}

View File

@@ -0,0 +1,374 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
// taken from https://github.com/rust-lang/cargo/blob/b0c9586f4cbf426914df47c65de38ea323772c74/src/cargo/util/flock.rs
#![allow(dead_code)]
use std::fs::{File, OpenOptions, create_dir_all};
use std::io;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use crate::{Error, Result, error::ErrorExt};
use sys::*;
#[derive(Debug)]
pub struct FileLock {
f: Option<File>,
path: PathBuf,
state: State,
}
#[derive(PartialEq, Debug)]
enum State {
Unlocked,
Shared,
Exclusive,
}
impl FileLock {
/// Returns the underlying file handle of this lock.
pub fn file(&self) -> &File {
self.f.as_ref().unwrap()
}
/// Returns the underlying path that this lock points to.
///
/// Note that special care must be taken to ensure that the path is not
/// referenced outside the lifetime of this lock.
pub fn path(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
&self.path
}
/// Returns the parent path containing this file
pub fn parent(&self) -> &Path {
assert_ne!(self.state, State::Unlocked);
self.path.parent().unwrap()
}
}
impl Read for FileLock {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.file().read(buf)
}
}
impl Seek for FileLock {
fn seek(&mut self, to: SeekFrom) -> io::Result<u64> {
self.file().seek(to)
}
}
impl Write for FileLock {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.file().write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.file().flush()
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if self.state != State::Unlocked {
if let Some(f) = self.f.take() {
let _ = unlock(&f);
}
}
}
}
/// Opens exclusive access to a file, returning the locked version of a
/// file.
///
/// This function will create a file at `path` if it doesn't already exist
/// (including intermediate directories), and then it will acquire an
/// exclusive lock on `path`. If the process must block waiting for the
/// lock, the `msg` is logged.
///
/// The returned file can be accessed to look at the path and also has
/// read/write access to the underlying file.
pub fn open_rw<P>(path: P, msg: &str) -> Result<FileLock>
where
P: AsRef<Path>,
{
open(
path.as_ref(),
OpenOptions::new().read(true).write(true).create(true),
State::Exclusive,
msg,
)
}
/// Opens shared access to a file, returning the locked version of a file.
///
/// This function will fail if `path` doesn't already exist, but if it does
/// then it will acquire a shared lock on `path`. If the process must block
/// waiting for the lock, the `msg` is logged.
///
/// The returned file can be accessed to look at the path and also has read
/// access to the underlying file. Any writes to the file will return an
/// error.
pub fn open_ro<P>(path: P, msg: &str) -> Result<FileLock>
where
P: AsRef<Path>,
{
open(
path.as_ref(),
OpenOptions::new().read(true),
State::Shared,
msg,
)
}
fn open(path: &Path, opts: &OpenOptions, state: State, msg: &str) -> Result<FileLock> {
// If we want an exclusive lock then if we fail because of NotFound it's
// likely because an intermediate directory didn't exist, so try to
// create the directory and then continue.
let f = opts.open(path).or_else(|e| {
if e.kind() == io::ErrorKind::NotFound && state == State::Exclusive {
create_dir_all(path.parent().unwrap()).fs_context(
"failed to create directory",
path.parent().unwrap().to_path_buf(),
)?;
Ok(
opts
.open(path)
.fs_context("failed to open file", path.to_path_buf())?,
)
} else {
Err(Error::Fs {
context: "failed to open file",
path: path.to_path_buf(),
error: e,
})
}
})?;
match state {
State::Exclusive => {
acquire(msg, path, &|| try_lock_exclusive(&f), &|| {
lock_exclusive(&f)
})?;
}
State::Shared => {
acquire(msg, path, &|| try_lock_shared(&f), &|| lock_shared(&f))?;
}
State::Unlocked => {}
}
Ok(FileLock {
f: Some(f),
path: path.to_path_buf(),
state,
})
}
/// Acquires a lock on a file in a "nice" manner.
///
/// Almost all long-running blocking actions in Cargo have a status message
/// associated with them as we're not sure how long they'll take. Whenever a
/// conflicted file lock happens, this is the case (we're not sure when the lock
/// will be released).
///
/// This function will acquire the lock on a `path`, printing out a nice message
/// to the console if we have to wait for it. It will first attempt to use `try`
/// to acquire a lock on the crate, and in the case of contention it will emit a
/// status message based on `msg` to `config`'s shell, and then use `block` to
/// block waiting to acquire a lock.
///
/// Returns an error if the lock could not be acquired or if any error other
/// than a contention error happens.
fn acquire(
msg: &str,
path: &Path,
lock_try: &dyn Fn() -> io::Result<()>,
lock_block: &dyn Fn() -> io::Result<()>,
) -> Result<()> {
// File locking on Unix is currently implemented via `flock`, which is known
// to be broken on NFS. We could in theory just ignore errors that happen on
// NFS, but apparently the failure mode [1] for `flock` on NFS is **blocking
// forever**, even if the "non-blocking" flag is passed!
//
// As a result, we just skip all file locks entirely on NFS mounts. That
// should avoid calling any `flock` functions at all, and it wouldn't work
// there anyway.
//
// [1]: https://github.com/rust-lang/cargo/issues/2615
if is_on_nfs_mount(path) {
return Ok(());
}
match lock_try() {
Ok(()) => return Ok(()),
// In addition to ignoring NFS which is commonly not working we also
// just ignore locking on filesystems that look like they don't
// implement file locking.
Err(e) if error_unsupported(&e) => return Ok(()),
Err(e) => {
if !error_contended(&e) {
return Err(Error::Fs {
context: "failed to lock file",
path: path.to_path_buf(),
error: e,
});
}
}
}
let msg = format!("waiting for file lock on {msg}");
log::info!(action = "Blocking"; "{msg}");
lock_block().fs_context("failed to lock file", path.to_path_buf())?;
return Ok(());
#[cfg(all(target_os = "linux", not(target_env = "musl")))]
fn is_on_nfs_mount(path: &Path) -> bool {
use std::ffi::CString;
use std::mem;
use std::os::unix::prelude::*;
let path = match CString::new(path.as_os_str().as_bytes()) {
Ok(path) => path,
Err(_) => return false,
};
unsafe {
let mut buf: libc::statfs = mem::zeroed();
let r = libc::statfs(path.as_ptr(), &mut buf);
r == 0 && buf.f_type as u32 == libc::NFS_SUPER_MAGIC as u32
}
}
#[cfg(any(not(target_os = "linux"), target_env = "musl"))]
fn is_on_nfs_mount(_path: &Path) -> bool {
false
}
}
#[cfg(unix)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::os::unix::io::AsRawFd;
pub(super) fn lock_shared(file: &File) -> Result<()> {
flock(file, libc::LOCK_SH)
}
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
flock(file, libc::LOCK_EX)
}
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
flock(file, libc::LOCK_SH | libc::LOCK_NB)
}
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
flock(file, libc::LOCK_EX | libc::LOCK_NB)
}
pub(super) fn unlock(file: &File) -> Result<()> {
flock(file, libc::LOCK_UN)
}
pub(super) fn error_contended(err: &Error) -> bool {
err.raw_os_error() == Some(libc::EWOULDBLOCK)
}
pub(super) fn error_unsupported(err: &Error) -> bool {
match err.raw_os_error() {
// Unfortunately, depending on the target, these may or may not be the same.
// For targets in which they are the same, the duplicate pattern causes a warning.
#[allow(unreachable_patterns)]
Some(libc::ENOTSUP | libc::EOPNOTSUPP) => true,
Some(libc::ENOSYS) => true,
_ => false,
}
}
#[cfg(not(target_os = "solaris"))]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
let ret = unsafe { libc::flock(file.as_raw_fd(), flag) };
if ret < 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(target_os = "solaris")]
fn flock(file: &File, flag: libc::c_int) -> Result<()> {
// Solaris lacks flock(), so simply succeed with a no-op
Ok(())
}
}
#[cfg(windows)]
mod sys {
use std::fs::File;
use std::io::{Error, Result};
use std::mem;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::{ERROR_INVALID_FUNCTION, ERROR_LOCK_VIOLATION, HANDLE};
use windows_sys::Win32::Storage::FileSystem::{
LOCK_FILE_FLAGS, LOCKFILE_EXCLUSIVE_LOCK, LOCKFILE_FAIL_IMMEDIATELY, LockFileEx, UnlockFile,
};
pub(super) fn lock_shared(file: &File) -> Result<()> {
lock_file(file, 0)
}
pub(super) fn lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK)
}
pub(super) fn try_lock_shared(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_FAIL_IMMEDIATELY)
}
pub(super) fn try_lock_exclusive(file: &File) -> Result<()> {
lock_file(file, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY)
}
pub(super) fn error_contended(err: &Error) -> bool {
err.raw_os_error() == Some(ERROR_LOCK_VIOLATION as i32)
}
pub(super) fn error_unsupported(err: &Error) -> bool {
err.raw_os_error() == Some(ERROR_INVALID_FUNCTION as i32)
}
pub(super) fn unlock(file: &File) -> Result<()> {
let ret = unsafe { UnlockFile(file.as_raw_handle() as HANDLE, 0, 0, !0, !0) };
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
fn lock_file(file: &File, flags: LOCK_FILE_FLAGS) -> Result<()> {
let ret = unsafe {
let mut overlapped = mem::zeroed();
LockFileEx(
file.as_raw_handle() as HANDLE,
flags,
0,
!0,
!0,
&mut overlapped,
)
};
if ret == 0 {
Err(Error::last_os_error())
} else {
Ok(())
}
}
}

View File

@@ -0,0 +1,131 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::fmt;
#[derive(Debug, Clone)]
pub enum Framework {
SolidJS,
SolidStart,
Svelte,
SvelteKit,
Angular,
React,
Nextjs,
Gatsby,
Nuxt,
Quasar,
VueCli,
Vue,
}
impl Framework {
pub fn dev_url(&self) -> String {
match self {
Self::SolidJS => "http://localhost:3000",
Self::SolidStart => "http://localhost:3000",
Self::Svelte => "http://localhost:8080",
Self::SvelteKit => "http://localhost:5173",
Self::Angular => "http://localhost:4200",
Self::React => "http://localhost:3000",
Self::Nextjs => "http://localhost:3000",
Self::Gatsby => "http://localhost:8000",
Self::Nuxt => "http://localhost:3000",
Self::Quasar => "http://localhost:8080",
Self::VueCli => "http://localhost:8080",
Self::Vue => "http://localhost:8080",
}
.into()
}
pub fn frontend_dist(&self) -> String {
match self {
Self::SolidJS => "../dist",
Self::SolidStart => "../dist/public",
Self::Svelte => "../public",
Self::SvelteKit => "../build",
Self::Angular => "../dist",
Self::React => "../build",
Self::Nextjs => "../out",
Self::Gatsby => "../public",
Self::Nuxt => "../dist",
Self::Quasar => "../dist/spa",
Self::VueCli => "../dist",
Self::Vue => "../dist",
}
.into()
}
}
impl fmt::Display for Framework {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SolidJS => write!(f, "SolidJS"),
Self::SolidStart => write!(f, "SolidStart"),
Self::Svelte => write!(f, "Svelte"),
Self::SvelteKit => write!(f, "SvelteKit"),
Self::Angular => write!(f, "Angular"),
Self::React => write!(f, "React"),
Self::Nextjs => write!(f, "React (Next.js)"),
Self::Gatsby => write!(f, "React (Gatsby)"),
Self::Nuxt => write!(f, "Vue.js (Nuxt)"),
Self::Quasar => write!(f, "Vue.js (Quasar)"),
Self::VueCli => write!(f, "Vue.js (Vue CLI)"),
Self::Vue => write!(f, "Vue.js"),
}
}
}
#[derive(Debug, Clone)]
pub enum Bundler {
Webpack,
Rollup,
Vite,
}
impl fmt::Display for Bundler {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Webpack => write!(f, "Webpack"),
Self::Rollup => write!(f, "Rollup"),
Self::Vite => write!(f, "Vite"),
}
}
}
pub fn infer_from_package_json(package_json: &str) -> (Option<Framework>, Option<Bundler>) {
let framework_map = [
("solid-start", Framework::SolidStart, Some(Bundler::Vite)),
("solid-js", Framework::SolidJS, Some(Bundler::Vite)),
("svelte", Framework::Svelte, Some(Bundler::Rollup)),
("@sveltejs/kit", Framework::SvelteKit, Some(Bundler::Vite)),
("@angular", Framework::Angular, Some(Bundler::Webpack)),
(r#""next""#, Framework::Nextjs, Some(Bundler::Webpack)),
("gatsby", Framework::Gatsby, Some(Bundler::Webpack)),
("react", Framework::React, None),
("nuxt", Framework::Nuxt, Some(Bundler::Webpack)),
("quasar", Framework::Quasar, Some(Bundler::Webpack)),
("@vue/cli", Framework::VueCli, Some(Bundler::Webpack)),
("vue", Framework::Vue, Some(Bundler::Vite)),
];
let bundler_map = [
("webpack", Bundler::Webpack),
("rollup", Bundler::Rollup),
("vite", Bundler::Vite),
];
let (framework, framework_bundler) = framework_map
.iter()
.find(|(keyword, _, _)| package_json.contains(keyword))
.map(|(_, framework, bundler)| (Some(framework.clone()), bundler.clone()))
.unwrap_or((None, None));
let bundler = bundler_map
.iter()
.find(|(keyword, _)| package_json.contains(keyword))
.map(|(_, bundler)| bundler.clone())
.or(framework_bundler);
(framework, bundler)
}

View File

@@ -0,0 +1,55 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use crate::{
Error,
error::{Context, ErrorExt},
};
use std::path::{Path, PathBuf};
pub fn copy_file(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
let from = from.as_ref();
let to = to.as_ref();
if !from.exists() {
Err(Error::Fs {
context: "failed to copy file",
path: from.to_path_buf(),
error: std::io::Error::new(std::io::ErrorKind::NotFound, "source does not exist"),
})?;
}
if !from.is_file() {
Err(Error::Fs {
context: "failed to copy file",
path: from.to_path_buf(),
error: std::io::Error::other("not a file"),
})?;
}
let dest_dir = to.parent().expect("No data in parent");
std::fs::create_dir_all(dest_dir)
.fs_context("failed to create directory", dest_dir.to_path_buf())?;
std::fs::copy(from, to).fs_context("failed to copy file", from.to_path_buf())?;
Ok(())
}
/// Find an entry in a directory matching a glob pattern.
/// Currently does not traverse subdirectories.
// currently only used on macOS
#[allow(dead_code)]
pub fn find_in_directory(path: &Path, glob_pattern: &str) -> crate::Result<PathBuf> {
let pattern = glob::Pattern::new(glob_pattern)
.with_context(|| format!("failed to parse glob pattern {glob_pattern}"))?;
for entry in std::fs::read_dir(path)
.with_context(|| format!("failed to read directory {}", path.display()))?
{
let entry = entry.context("failed to read directory entry")?;
if pattern.matches_path(&entry.path()) {
return Ok(entry.path());
}
}
crate::error::bail!(
"No file found in {} matching {}",
path.display(),
glob_pattern
)
}

View File

@@ -0,0 +1,26 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use ureq::{Agent, Body, http::Response};
const CLI_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
pub fn get(url: &str) -> Result<Response<Body>, ureq::Error> {
#[allow(unused_mut)]
let mut config_builder = ureq::Agent::config_builder()
.user_agent(CLI_USER_AGENT)
.proxy(ureq::Proxy::try_from_env());
#[cfg(feature = "platform-certs")]
{
config_builder = config_builder.tls_config(
ureq::tls::TlsConfig::builder()
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
.build(),
);
}
let agent: Agent = config_builder.build().into();
agent.get(url).call()
}

View File

@@ -0,0 +1,52 @@
{
"16x16": {
"name": "icon_16x16.png",
"size": 16,
"ostype": "is32"
},
"16x16@2x": {
"name": "icon_16x16@2x.png",
"size": 32,
"ostype": "ic11"
},
"32x32": {
"name": "icon_32x32.png",
"size": 32,
"ostype": "il32"
},
"32x32@2x": {
"name": "icon_32x32@2x.png",
"size": 64,
"ostype": "ic12"
},
"128x128": {
"name": "icon_128x128.png",
"size": 128,
"ostype": "ic07"
},
"128x128@2x": {
"name": "icon_128x128@2x.png",
"size": 256,
"ostype": "ic13"
},
"256x256": {
"name": "icon_256x256.png",
"size": 256,
"ostype": "ic08"
},
"256x256@2x": {
"name": "icon_256x256@2x.png",
"size": 512,
"ostype": "ic14"
},
"512x512": {
"name": "icon_512x512.png",
"size": 512,
"ostype": "ic09"
},
"512x512@2x": {
"name": "icon_512x512@2x.png",
"size": 1024,
"ostype": "ic10"
}
}

View File

@@ -0,0 +1,152 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
pub mod app_paths;
pub mod cargo;
pub mod cargo_manifest;
pub mod config;
pub mod flock;
pub mod framework;
pub mod fs;
pub mod http;
pub mod npm;
#[cfg(target_os = "macos")]
pub mod pbxproj;
#[cfg(target_os = "macos")]
pub mod plist;
pub mod plugins;
pub mod prompts;
pub mod template;
pub mod updater_signature;
use std::{
collections::HashMap,
path::{Path, PathBuf},
process::Command,
};
use tauri_utils::config::HookCommand;
#[cfg(not(target_os = "windows"))]
use crate::Error;
use crate::{CommandExt, interface::AppInterface};
pub fn command_env(debug: bool) -> HashMap<&'static str, String> {
let mut map = HashMap::new();
map.insert(
"TAURI_ENV_PLATFORM_VERSION",
os_info::get().version().to_string(),
);
if debug {
map.insert("TAURI_ENV_DEBUG", "true".into());
}
map
}
pub fn resolve_tauri_path<P: AsRef<Path>>(path: P, crate_name: &str) -> PathBuf {
let path = path.as_ref();
if path.is_absolute() {
path.join(crate_name)
} else {
PathBuf::from("..").join(path).join(crate_name)
}
}
pub fn cross_command(bin: &str) -> Command {
#[cfg(target_os = "windows")]
let cmd = {
let mut cmd = Command::new("cmd");
cmd.arg("/c").arg(bin);
cmd
};
#[cfg(not(target_os = "windows"))]
let cmd = Command::new(bin);
cmd
}
pub fn run_hook(
name: &str,
hook: HookCommand,
interface: &AppInterface,
debug: bool,
frontend_dir: &Path,
) -> crate::Result<()> {
let (script, script_cwd) = match hook {
HookCommand::Script(s) if s.is_empty() => (None, None),
HookCommand::Script(s) => (Some(s), None),
HookCommand::ScriptWithOptions { script, cwd } => (Some(script), cwd.map(Into::into)),
};
let cwd = script_cwd.unwrap_or_else(|| frontend_dir.to_owned());
if let Some(script) = script {
log::info!(action = "Running"; "{} `{}`", name, script);
let mut env = command_env(debug);
env.extend(interface.env());
log::debug!("Setting environment for hook {:?}", env);
#[cfg(target_os = "windows")]
let status = Command::new("cmd")
.arg("/S")
.arg("/C")
.arg(&script)
.current_dir(cwd)
.envs(env)
.piped()
.map_err(|error| crate::error::Error::CommandFailed {
command: script.clone(),
error,
})?;
#[cfg(not(target_os = "windows"))]
let status = Command::new("sh")
.arg("-c")
.arg(&script)
.current_dir(cwd)
.envs(env)
.piped()
.map_err(|error| Error::CommandFailed {
command: script.clone(),
error,
})?;
if !status.success() {
crate::error::bail!(
"{} `{}` failed with exit code {}",
name,
script,
status.code().unwrap_or_default()
);
}
}
Ok(())
}
#[cfg(target_os = "macos")]
pub fn strip_semver_prerelease_tag(version: &mut semver::Version) -> crate::Result<()> {
use crate::error::Context;
if !version.pre.is_empty() {
if let Some((_prerelease_tag, number)) = version.pre.as_str().to_string().split_once('.') {
version.pre = semver::Prerelease::EMPTY;
version.build = semver::BuildMetadata::new(&format!(
"{prefix}{number}",
prefix = if version.build.is_empty() {
"".to_string()
} else {
format!(".{}", version.build.as_str())
}
))
.with_context(|| {
format!(
"failed to parse {version} as semver: bundle version {number:?} prerelease is invalid"
)
})?;
}
}
Ok(())
}

View File

@@ -0,0 +1,459 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use serde::Deserialize;
use crate::{
error::{Context, Error},
helpers::cross_command,
};
use std::{collections::HashMap, fmt::Display, path::Path, process::Command};
pub fn manager_version(package_manager: &str) -> Option<String> {
cross_command(package_manager)
.arg("-v")
.output()
.map(|o| {
if o.status.success() {
let v = String::from_utf8_lossy(o.stdout.as_slice()).to_string();
Some(v.split('\n').next().unwrap().to_string())
} else {
None
}
})
.ok()
.unwrap_or_default()
}
fn detect_yarn_or_berry() -> PackageManager {
if manager_version("yarn")
.map(|v| v.chars().next().map(|c| c > '1').unwrap_or_default())
.unwrap_or(false)
{
PackageManager::YarnBerry
} else {
PackageManager::Yarn
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum PackageManager {
Npm,
Pnpm,
Yarn,
YarnBerry,
Bun,
Deno,
}
impl Display for PackageManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
PackageManager::Npm => "npm",
PackageManager::Pnpm => "pnpm",
PackageManager::Yarn => "yarn",
PackageManager::YarnBerry => "yarn berry",
PackageManager::Bun => "bun",
PackageManager::Deno => "deno",
}
)
}
}
impl PackageManager {
/// Detects package manager from the given directory, falls back to [`PackageManager::Npm`].
pub fn from_project<P: AsRef<Path>>(path: P) -> Self {
Self::all_from_project(path)
.first()
.copied()
.unwrap_or(Self::Npm)
}
/// Detects package manager from the `npm_config_user_agent` environment variable
fn from_environment_variable() -> Option<Self> {
let npm_config_user_agent = std::env::var("npm_config_user_agent").ok()?;
match npm_config_user_agent {
user_agent if user_agent.starts_with("pnpm/") => Some(Self::Pnpm),
user_agent if user_agent.starts_with("deno/") => Some(Self::Deno),
user_agent if user_agent.starts_with("bun/") => Some(Self::Bun),
user_agent if user_agent.starts_with("yarn/") => Some(detect_yarn_or_berry()),
user_agent if user_agent.starts_with("npm/") => Some(Self::Npm),
_ => None,
}
}
/// Detects all possible package managers from the given directory.
pub fn all_from_project<P: AsRef<Path>>(path: P) -> Vec<Self> {
if let Some(from_env) = Self::from_environment_variable() {
return vec![from_env];
}
let mut found = Vec::new();
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
let path = entry.path();
let name = path.file_name().unwrap().to_string_lossy();
match name.as_ref() {
"package-lock.json" => found.push(PackageManager::Npm),
"pnpm-lock.yaml" => found.push(PackageManager::Pnpm),
"yarn.lock" => found.push(detect_yarn_or_berry()),
"bun.lock" | "bun.lockb" => found.push(PackageManager::Bun),
"deno.lock" => found.push(PackageManager::Deno),
_ => (),
}
}
}
found
}
fn cross_command(&self) -> Command {
match self {
PackageManager::Yarn => cross_command("yarn"),
PackageManager::YarnBerry => cross_command("yarn"),
PackageManager::Npm => cross_command("npm"),
PackageManager::Pnpm => cross_command("pnpm"),
PackageManager::Bun => cross_command("bun"),
PackageManager::Deno => cross_command("deno"),
}
}
pub fn install<P: AsRef<Path>>(
&self,
dependencies: &[String],
frontend_dir: P,
) -> crate::Result<()> {
let dependencies_str = if dependencies.len() > 1 {
"dependencies"
} else {
"dependency"
};
log::info!(
"Installing NPM {dependencies_str} {}...",
dependencies
.iter()
.map(|d| format!("\"{d}\""))
.collect::<Vec<_>>()
.join(", ")
);
let mut command = self.cross_command();
command.arg("add");
match self {
PackageManager::Deno => command.args(dependencies.iter().map(|d| format!("npm:{d}"))),
_ => command.args(dependencies),
};
let status = command
.current_dir(frontend_dir)
.status()
.map_err(|error| Error::CommandFailed {
command: format!("failed to run {self}"),
error,
})?;
if !status.success() {
crate::error::bail!("Failed to install NPM {dependencies_str}");
}
Ok(())
}
pub fn remove<P: AsRef<Path>>(
&self,
dependencies: &[String],
frontend_dir: P,
) -> crate::Result<()> {
let dependencies_str = if dependencies.len() > 1 {
"dependencies"
} else {
"dependency"
};
log::info!(
"Removing NPM {dependencies_str} {}...",
dependencies
.iter()
.map(|d| format!("\"{d}\""))
.collect::<Vec<_>>()
.join(", ")
);
let status = self
.cross_command()
.arg(if *self == Self::Npm {
"uninstall"
} else {
"remove"
})
.args(dependencies)
.current_dir(frontend_dir)
.status()
.map_err(|error| Error::CommandFailed {
command: format!("failed to run {self}"),
error,
})?;
if !status.success() {
crate::error::bail!("Failed to remove NPM {dependencies_str}");
}
Ok(())
}
// TODO: Use `current_package_versions` as much as possible for better speed
pub fn current_package_version<P: AsRef<Path>>(
&self,
name: &str,
frontend_dir: P,
) -> crate::Result<Option<String>> {
let (output, regex) = match self {
PackageManager::Yarn => (
cross_command("yarn")
.args(["list", "--pattern"])
.arg(name)
.args(["--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "yarn list --pattern".to_string(),
error,
})?,
None,
),
PackageManager::YarnBerry => (
cross_command("yarn")
.arg("info")
.arg(name)
.arg("--json")
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "yarn info --json".to_string(),
error,
})?,
Some(regex::Regex::new("\"Version\":\"([\\da-zA-Z\\-\\.]+)\"").unwrap()),
),
PackageManager::Pnpm => (
cross_command("pnpm")
.arg("list")
.arg(name)
.args(["--parseable", "--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "pnpm list --parseable --depth 0".to_string(),
error,
})?,
None,
),
// Bun and Deno don't support `list` command
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => (
cross_command("npm")
.arg("list")
.arg(name)
.args(["version", "--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "npm list --version --depth 0".to_string(),
error,
})?,
None,
),
};
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
let regex = regex.unwrap_or_else(|| regex::Regex::new("@(\\d[\\da-zA-Z\\-\\.]+)").unwrap());
Ok(
regex
.captures_iter(&stdout)
.last()
.and_then(|cap| cap.get(1).map(|v| v.as_str().to_string())),
)
} else {
Ok(None)
}
}
pub fn current_package_versions(
&self,
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = match self {
PackageManager::Yarn => return yarn_package_versions(packages, frontend_dir),
PackageManager::YarnBerry => return yarn_berry_package_versions(packages, frontend_dir),
PackageManager::Pnpm => cross_command("pnpm")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "pnpm list --json --depth 0".to_string(),
error,
})?,
// Bun and Deno don't support `list` command
PackageManager::Npm | PackageManager::Bun | PackageManager::Deno => cross_command("npm")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "npm list --json --depth 0".to_string(),
error,
})?,
};
let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ListOutput {
#[serde(default)]
dependencies: HashMap<String, ListDependency>,
#[serde(default)]
dev_dependencies: HashMap<String, ListDependency>,
}
#[derive(Deserialize)]
struct ListDependency {
version: String,
}
let json = if matches!(self, PackageManager::Pnpm) {
serde_json::from_str::<Vec<ListOutput>>(&stdout)
.ok()
.and_then(|out| out.into_iter().next())
.context("failed to parse pnpm list")?
} else {
serde_json::from_str::<ListOutput>(&stdout).context("failed to parse npm list")?
};
for (package, dependency) in json.dependencies.into_iter().chain(json.dev_dependencies) {
let version = dependency.version;
if let Ok(version) = semver::Version::parse(&version) {
versions.insert(package, version);
} else {
log::debug!("Failed to parse version `{version}` for NPM package `{package}`");
}
}
Ok(versions)
}
}
fn yarn_package_versions(
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = cross_command("yarn")
.arg("list")
.args(packages)
.args(["--json", "--depth", "0"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "yarn list --json --depth 0".to_string(),
error,
})?;
let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}
#[derive(Deserialize)]
struct YarnListOutput {
data: YarnListOutputData,
}
#[derive(Deserialize)]
struct YarnListOutputData {
trees: Vec<YarnListOutputDataTree>,
}
#[derive(Deserialize)]
struct YarnListOutputDataTree {
name: String,
}
for line in stdout.lines() {
if let Ok(tree) = serde_json::from_str::<YarnListOutput>(line) {
for tree in tree.data.trees {
let Some((name, version)) = tree.name.rsplit_once('@') else {
continue;
};
if let Ok(version) = semver::Version::parse(version) {
versions.insert(name.to_owned(), version);
} else {
log::debug!("Failed to parse version `{version}` for NPM package `{name}`");
}
}
return Ok(versions);
}
}
Ok(versions)
}
fn yarn_berry_package_versions(
packages: &[String],
frontend_dir: &Path,
) -> crate::Result<HashMap<String, semver::Version>> {
let output = cross_command("yarn")
.args(["info", "--json"])
.current_dir(frontend_dir)
.output()
.map_err(|error| Error::CommandFailed {
command: "yarn info --json".to_string(),
error,
})?;
let mut versions = HashMap::new();
let stdout = String::from_utf8_lossy(&output.stdout);
if !output.status.success() {
return Ok(versions);
}
#[derive(Deserialize)]
struct YarnBerryInfoOutput {
value: String,
children: YarnBerryInfoOutputChildren,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
struct YarnBerryInfoOutputChildren {
version: String,
}
for line in stdout.lines() {
if let Ok(info) = serde_json::from_str::<YarnBerryInfoOutput>(line) {
let Some((name, _)) = info.value.rsplit_once('@') else {
continue;
};
if !packages.iter().any(|package| package == name) {
continue;
}
let version = info.children.version;
if let Ok(version) = semver::Version::parse(&version) {
versions.insert(name.to_owned(), version);
} else {
log::debug!("Failed to parse version `{version}` for NPM package `{name}`");
}
}
}
Ok(versions)
}

View File

@@ -0,0 +1,327 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{
collections::BTreeMap,
fmt,
path::{Path, PathBuf},
};
use crate::error::ErrorExt;
pub fn parse<P: AsRef<Path>>(path: P) -> crate::Result<Pbxproj> {
let path = path.as_ref();
let pbxproj =
std::fs::read_to_string(path).fs_context("failed to read pbxproj file", path.to_path_buf())?;
let mut proj = Pbxproj {
path: path.to_owned(),
raw_lines: pbxproj.split('\n').map(ToOwned::to_owned).collect(),
xc_build_configuration: BTreeMap::new(),
xc_configuration_list: BTreeMap::new(),
additions: BTreeMap::new(),
has_changes: false,
};
let mut state = State::Idle;
let mut iter = proj.raw_lines.iter().enumerate();
while let Some((line_number, line)) = iter.next() {
match &state {
State::Idle => {
if line == "/* Begin XCBuildConfiguration section */" {
state = State::XCBuildConfiguration;
} else if line == "/* Begin XCConfigurationList section */" {
state = State::XCConfigurationList;
}
}
// XCBuildConfiguration
State::XCBuildConfiguration => {
if line == "/* End XCBuildConfiguration section */" {
state = State::Idle;
} else if let Some((_identation, token)) = split_at_identation(line) {
let id: String = token.chars().take_while(|c| !c.is_whitespace()).collect();
proj.xc_build_configuration.insert(
id.clone(),
XCBuildConfiguration {
build_settings: Vec::new(),
},
);
state = State::XCBuildConfigurationObject { id };
}
}
State::XCBuildConfigurationObject { id } => {
if line.contains("buildSettings") {
state = State::XCBuildConfigurationObjectBuildSettings { id: id.clone() };
} else if split_at_identation(line).is_some_and(|(_ident, token)| token == "};") {
state = State::XCBuildConfiguration;
}
}
State::XCBuildConfigurationObjectBuildSettings { id } => {
if let Some((identation, token)) = split_at_identation(line) {
if token == "};" {
state = State::XCBuildConfigurationObject { id: id.clone() };
} else {
let assignment = token.trim_end_matches(';');
if let Some((key, value)) = assignment.split_once(" = ") {
// multiline value
let value = if value == "(" {
let mut value = value.to_string();
for (_next_line_number, next_line) in iter.by_ref() {
value.push_str(next_line);
value.push('\n');
if let Some((_, token)) = split_at_identation(next_line) {
if token == ");" {
break;
}
}
}
value
} else {
value.trim().to_string()
};
proj
.xc_build_configuration
.get_mut(id)
.unwrap()
.build_settings
.push(BuildSettings {
identation: identation.into(),
line_number,
key: key.trim().into(),
value,
});
}
}
}
}
// XCConfigurationList
State::XCConfigurationList => {
if line == "/* End XCConfigurationList section */" {
state = State::Idle;
} else if let Some((_identation, token)) = split_at_identation(line) {
let Some((id, comment)) = token.split_once(' ') else {
continue;
};
proj.xc_configuration_list.insert(
id.to_string(),
XCConfigurationList {
comment: comment.trim_end_matches(" = {").to_string(),
build_configurations: Vec::new(),
},
);
state = State::XCConfigurationListObject { id: id.to_string() };
}
}
State::XCConfigurationListObject { id } => {
if line.contains("buildConfigurations") {
state = State::XCConfigurationListObjectBuildConfigurations { id: id.clone() };
} else if split_at_identation(line).is_some_and(|(_ident, token)| token == "};") {
state = State::XCConfigurationList;
}
}
State::XCConfigurationListObjectBuildConfigurations { id } => {
if let Some((_identation, token)) = split_at_identation(line) {
if token == ");" {
state = State::XCConfigurationListObject { id: id.clone() };
} else {
let Some((build_configuration_id, comments)) = token.split_once(' ') else {
continue;
};
proj
.xc_configuration_list
.get_mut(id)
.unwrap()
.build_configurations
.push(BuildConfigurationRef {
id: build_configuration_id.to_string(),
comments: comments.trim_end_matches(',').to_string(),
});
}
}
}
}
}
Ok(proj)
}
fn split_at_identation(s: &str) -> Option<(&str, &str)> {
s.chars()
.position(|c| !c.is_ascii_whitespace())
.map(|pos| s.split_at(pos))
}
enum State {
Idle,
// XCBuildConfiguration
XCBuildConfiguration,
XCBuildConfigurationObject { id: String },
XCBuildConfigurationObjectBuildSettings { id: String },
// XCConfigurationList
XCConfigurationList,
XCConfigurationListObject { id: String },
XCConfigurationListObjectBuildConfigurations { id: String },
}
pub struct Pbxproj {
pub path: PathBuf,
raw_lines: Vec<String>,
pub xc_build_configuration: BTreeMap<String, XCBuildConfiguration>,
pub xc_configuration_list: BTreeMap<String, XCConfigurationList>,
// maps the line number to the line to add
additions: BTreeMap<usize, String>,
has_changes: bool,
}
impl fmt::Debug for Pbxproj {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Pbxproj")
.field("xc_build_configuration", &self.xc_build_configuration)
.field("xc_configuration_list", &self.xc_configuration_list)
.finish()
}
}
impl Pbxproj {
pub fn has_changes(&self) -> bool {
!self.additions.is_empty() || self.has_changes
}
fn serialize(&self) -> String {
let mut proj = String::new();
let last_line_number = self.raw_lines.len() - 1;
for (number, line) in self.raw_lines.iter().enumerate() {
if let Some(new) = self.additions.get(&number) {
proj.push_str(new);
proj.push('\n');
}
proj.push_str(line);
if number != last_line_number {
proj.push('\n');
}
}
proj
}
pub fn save(&self) -> std::io::Result<()> {
std::fs::write(&self.path, self.serialize())
}
pub fn set_build_settings(&mut self, build_configuration_id: &str, key: &str, value: &str) {
let Some(build_configuration) = self.xc_build_configuration.get_mut(build_configuration_id)
else {
return;
};
if let Some(build_setting) = build_configuration
.build_settings
.iter_mut()
.find(|s| s.key == key)
{
if build_setting.value != value {
let Some(line) = self.raw_lines.get_mut(build_setting.line_number) else {
return;
};
*line = format!("{}{key} = {value};", build_setting.identation);
self.has_changes = true;
}
} else {
let Some(last_build_setting) = build_configuration.build_settings.last().cloned() else {
return;
};
build_configuration.build_settings.push(BuildSettings {
identation: last_build_setting.identation.clone(),
line_number: last_build_setting.line_number + 1,
key: key.to_string(),
value: value.to_string(),
});
self.additions.insert(
last_build_setting.line_number + 1,
format!("{}{key} = {value};", last_build_setting.identation),
);
}
}
}
#[derive(Debug)]
pub struct XCBuildConfiguration {
build_settings: Vec<BuildSettings>,
}
impl XCBuildConfiguration {
pub fn get_build_setting(&self, key: &str) -> Option<&BuildSettings> {
self.build_settings.iter().find(|s| s.key == key)
}
}
#[derive(Debug, Clone)]
pub struct BuildSettings {
identation: String,
line_number: usize,
pub key: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct XCConfigurationList {
pub comment: String,
pub build_configurations: Vec<BuildConfigurationRef>,
}
#[derive(Debug, Clone)]
pub struct BuildConfigurationRef {
pub id: String,
pub comments: String,
}
#[cfg(test)]
mod tests {
#[test]
fn parse() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let fixtures_path = manifest_dir.join("tests").join("fixtures").join("pbxproj");
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(fixtures_path.join("snapshots"));
let _guard = settings.bind_to_scope();
insta::assert_debug_snapshot!(
"project.pbxproj",
super::parse(fixtures_path.join("project.pbxproj")).expect("failed to parse pbxproj")
);
}
#[test]
fn modify() {
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
let fixtures_path = manifest_dir.join("tests").join("fixtures").join("pbxproj");
let mut settings = insta::Settings::clone_current();
settings.set_snapshot_path(fixtures_path.join("snapshots"));
let _guard = settings.bind_to_scope();
let mut pbxproj =
super::parse(fixtures_path.join("project.pbxproj")).expect("failed to parse pbxproj");
pbxproj.set_build_settings(
"DB_0E254D0FD84970B57F6410",
"PRODUCT_NAME",
"\"Tauri Test\"",
);
pbxproj.set_build_settings("DB_0E254D0FD84970B57F6410", "UNKNOWN", "9283j49238h");
insta::assert_snapshot!("project-modified.pbxproj", pbxproj.serialize());
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use crate::error::Context;
pub enum PlistKind {
Path(PathBuf),
Plist(plist::Value),
}
impl From<PathBuf> for PlistKind {
fn from(p: PathBuf) -> Self {
Self::Path(p)
}
}
impl From<plist::Value> for PlistKind {
fn from(p: plist::Value) -> Self {
Self::Plist(p)
}
}
pub fn merge_plist(src: Vec<PlistKind>) -> crate::Result<plist::Value> {
let mut merged_plist = plist::Dictionary::new();
for plist_kind in src {
let src_plist = match plist_kind {
PlistKind::Path(p) => plist::Value::from_file(&p)
.with_context(|| format!("failed to parse plist from {}", p.display()))?,
PlistKind::Plist(v) => v,
};
if let Some(dict) = src_plist.into_dictionary() {
for (key, value) in dict {
merged_plist.insert(key, value);
}
}
}
Ok(plist::Value::Dictionary(merged_plist))
}

View File

@@ -0,0 +1,98 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::collections::HashMap;
#[derive(Default)]
pub struct PluginMetadata {
pub desktop_only: bool,
pub mobile_only: bool,
pub rust_only: bool,
pub builder: bool,
pub version_req: Option<String>,
}
// known plugins with particular cases
pub fn known_plugins() -> HashMap<&'static str, PluginMetadata> {
let mut plugins: HashMap<&'static str, PluginMetadata> = HashMap::new();
// desktop-only
for p in [
"authenticator",
"autostart",
"cli",
"global-shortcut",
"positioner",
"single-instance",
"updater",
"window-state",
] {
plugins.entry(p).or_default().desktop_only = true;
}
// mobile-only
for p in ["barcode-scanner", "biometric", "nfc", "haptics"] {
plugins.entry(p).or_default().mobile_only = true;
}
// uses builder pattern
for p in [
"autostart",
"global-shortcut",
"localhost",
"log",
"sql",
"store",
"stronghold",
"updater",
"window-state",
] {
plugins.entry(p).or_default().builder = true;
}
// rust-only
#[allow(clippy::single_element_loop)]
for p in ["localhost", "persisted-scope", "single-instance"] {
plugins.entry(p).or_default().rust_only = true;
}
// known, but no particular config
for p in [
"geolocation",
"deep-link",
"dialog",
"fs",
"http",
"notification",
"os",
"process",
"shell",
"upload",
"websocket",
"opener",
"clipboard-manager",
] {
plugins.entry(p).or_default();
}
let version_req = version_req();
for plugin in plugins.values_mut() {
plugin.version_req.replace(version_req.clone());
}
plugins
}
fn version_req() -> String {
let pre = env!("CARGO_PKG_VERSION_PRE");
if pre.is_empty() {
env!("CARGO_PKG_VERSION_MAJOR").to_string()
} else {
format!(
"{}.0.0-{}",
env!("CARGO_PKG_VERSION_MAJOR"),
pre.split('.').next().unwrap()
)
}
}

View File

@@ -0,0 +1,61 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{fmt::Display, str::FromStr};
use crate::{Result, error::Context};
pub fn input<T>(
prompt: &str,
initial: Option<T>,
skip: bool,
allow_empty: bool,
) -> Result<Option<T>>
where
T: Clone + FromStr + Display + ToString,
T::Err: Display + std::fmt::Debug,
T: PartialEq<str>,
{
if skip {
Ok(initial)
} else {
let theme = dialoguer::theme::ColorfulTheme::default();
let mut builder = dialoguer::Input::with_theme(&theme)
.with_prompt(prompt)
.allow_empty(allow_empty);
if let Some(v) = initial {
builder = builder.with_initial_text(v.to_string());
}
builder
.interact_text()
.map(|t: T| if t.ne("") { Some(t) } else { None })
.context("failed to prompt input")
}
}
pub fn confirm(prompt: &str, default: Option<bool>) -> Result<bool> {
let theme = dialoguer::theme::ColorfulTheme::default();
let mut builder = dialoguer::Confirm::with_theme(&theme).with_prompt(prompt);
if let Some(default) = default {
builder = builder.default(default);
}
builder.interact().context("failed to prompt confirm")
}
pub fn multiselect<T: ToString, I: IntoIterator<Item = T>>(
prompt: &str,
items: I,
defaults: Option<&[bool]>,
) -> Result<Vec<usize>> {
let theme = dialoguer::theme::ColorfulTheme::default();
let mut builder = dialoguer::MultiSelect::with_theme(&theme)
.with_prompt(prompt)
.items(items);
if let Some(defaults) = defaults {
builder = builder.defaults(defaults);
}
builder.interact().context("failed to prompt multi-select")
}

View File

@@ -0,0 +1,97 @@
// 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::Write,
path::{Path, PathBuf},
};
use handlebars::{Handlebars, to_json};
use include_dir::Dir;
use serde::Serialize;
use serde_json::value::{Map, Value as JsonValue};
use crate::error::ErrorExt;
/// Map of template variable names and values.
#[derive(Clone, Debug)]
#[repr(transparent)]
pub struct JsonMap(Map<String, JsonValue>);
impl Default for JsonMap {
fn default() -> Self {
Self(Map::new())
}
}
impl JsonMap {
pub fn insert(&mut self, name: &str, value: impl Serialize) {
self.0.insert(name.to_owned(), to_json(value));
}
pub fn inner(&self) -> &Map<String, JsonValue> {
&self.0
}
}
pub fn render<P: AsRef<Path>, D: Serialize>(
handlebars: &Handlebars<'_>,
data: &D,
dir: &Dir<'_>,
out_dir: P,
) -> crate::Result<()> {
let out_dir = out_dir.as_ref();
let mut created_dirs = Vec::new();
render_with_generator(handlebars, data, dir, out_dir, &mut |file_path: PathBuf| {
let path = out_dir.join(file_path);
let parent = path.parent().unwrap().to_path_buf();
if !created_dirs.contains(&parent) {
create_dir_all(&parent)?;
created_dirs.push(parent);
}
File::create(path).map(Some)
})
}
pub fn render_with_generator<
P: AsRef<Path>,
D: Serialize,
F: FnMut(PathBuf) -> std::io::Result<Option<File>>,
>(
handlebars: &Handlebars<'_>,
data: &D,
dir: &Dir<'_>,
out_dir: P,
out_file_generator: &mut F,
) -> crate::Result<()> {
let out_dir = out_dir.as_ref();
for file in dir.files() {
let mut file_path = file.path().to_path_buf();
// cargo for some reason ignores the /templates folder packaging when it has a Cargo.toml file inside
// so we rename the extension to `.crate-manifest`
if let Some(extension) = file_path.extension() {
if extension == "crate-manifest" {
file_path.set_extension("toml");
}
}
if let Some(mut output_file) = out_file_generator(file_path.clone())
.fs_context("failed to generate output file", file_path.clone())?
{
if let Some(utf8) = file.contents_utf8() {
handlebars
.render_template_to_write(utf8, &data, &mut output_file)
.expect("Failed to render template");
} else {
output_file
.write_all(file.contents())
.fs_context("failed to write template", file_path.clone())?;
}
}
}
for dir in dir.dirs() {
render_with_generator(handlebars, data, dir, out_dir, out_file_generator)?;
}
Ok(())
}

View File

@@ -0,0 +1,297 @@
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use base64::Engine;
use minisign::{
KeyPair as KP, PublicKey, PublicKeyBox, SecretKey, SecretKeyBox, SignatureBox, sign,
};
use std::{
fs::{self, File, OpenOptions},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
str,
time::{SystemTime, UNIX_EPOCH},
};
use crate::error::{Context, ErrorExt};
/// A key pair (`PublicKey` and `SecretKey`).
#[derive(Clone, Debug)]
pub struct KeyPair {
pub pk: String,
pub sk: String,
}
fn create_file(path: &Path) -> crate::Result<BufWriter<File>> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).fs_context("failed to create directory", parent.to_path_buf())?;
}
let file = File::create(path).fs_context("failed to create file", path.to_path_buf())?;
Ok(BufWriter::new(file))
}
/// Generate base64 encoded keypair
pub fn generate_key(password: Option<String>) -> crate::Result<KeyPair> {
let KP { pk, sk } = KP::generate_encrypted_keypair(password).unwrap();
let pk_box_str = pk.to_box().unwrap().to_string();
let sk_box_str = sk.to_box(None).unwrap().to_string();
let encoded_pk = base64::engine::general_purpose::STANDARD.encode(pk_box_str);
let encoded_sk = base64::engine::general_purpose::STANDARD.encode(sk_box_str);
Ok(KeyPair {
pk: encoded_pk,
sk: encoded_sk,
})
}
/// Transform a base64 String to readable string for the main signer
pub fn decode_key<S: AsRef<[u8]>>(base64_key: S) -> crate::Result<String> {
let decoded_str = &base64::engine::general_purpose::STANDARD
.decode(base64_key)
.context("failed to decode base64 key")?[..];
Ok(String::from(
str::from_utf8(decoded_str).context("failed to convert base64 to utf8")?,
))
}
/// Save KeyPair to disk
pub fn save_keypair<P>(
force: bool,
sk_path: P,
key: &str,
pubkey: &str,
) -> crate::Result<(PathBuf, PathBuf)>
where
P: AsRef<Path>,
{
let sk_path = sk_path.as_ref();
let pubkey_path = format!("{}.pub", sk_path.display());
let pk_path = Path::new(&pubkey_path);
if sk_path.exists() {
if !force {
crate::error::bail!(
"Key generation aborted:\n{} already exists\nIf you really want to overwrite the existing key pair, add the --force switch to force this operation.",
sk_path.display()
);
} else {
std::fs::remove_file(sk_path)
.fs_context("failed to remove secret key file", sk_path.to_path_buf())?;
}
}
if pk_path.exists() {
std::fs::remove_file(pk_path)
.fs_context("failed to remove public key file", pk_path.to_path_buf())?;
}
let write_file = |mut writer: BufWriter<File>, contents: &str| -> std::io::Result<()> {
write!(writer, "{contents:}")?;
writer.flush()?;
Ok(())
};
write_file(create_file(sk_path)?, key)
.fs_context("failed to write secret key", sk_path.to_path_buf())?;
write_file(create_file(pk_path)?, pubkey)
.fs_context("failed to write public key", pk_path.to_path_buf())?;
Ok((
fs::canonicalize(sk_path).fs_context(
"failed to canonicalize secret key path",
sk_path.to_path_buf(),
)?,
fs::canonicalize(pk_path).fs_context(
"failed to canonicalize public key path",
pk_path.to_path_buf(),
)?,
))
}
/// Sign files
///
/// When `version` is given it is embedded in the signature's trusted comment. minisign covers
/// the trusted comment with its global signature, so this binds the signed artifact to the
/// version it was released as. The update manifest is not itself signed, so without this the
/// manifest's `version` field can be paired with an older release's URL and signature to force
/// a downgrade. See the `requireSignedVersion` updater config option.
pub fn sign_file<P>(
secret_key: &SecretKey,
bin_path: P,
version: Option<&str>,
) -> crate::Result<(PathBuf, SignatureBox)>
where
P: AsRef<Path>,
{
let bin_path = bin_path.as_ref();
// We need to append .sig at the end it's where the signature will be stored
// TODO: use `with_added_extension` when we bump MSRV to >= 1.91
let signature_path = if let Some(ext) = bin_path.extension() {
let mut extension = ext.to_os_string();
extension.push(".sig");
bin_path.with_extension(extension)
} else {
bin_path.with_extension("sig")
};
let mut trusted_comment = format!(
"timestamp:{}\tfile:{}",
unix_timestamp(),
bin_path.file_name().unwrap().to_string_lossy()
);
if let Some(version) = version {
// the trusted comment is a single line of tab separated fields, so a version carrying
// either separator would produce a signature we cannot parse back
if version.contains(['\t', '\r', '\n']) {
crate::error::bail!(
"the app version {version:?} cannot be signed because it contains a tab or newline"
);
}
// appended last so anything parsing the historical `timestamp:...\tfile:...` prefix keeps working
trusted_comment.push_str("\tversion:");
trusted_comment.push_str(version);
}
let data_reader = open_data_file(bin_path)?;
let signature_box = sign(
None,
secret_key,
data_reader,
Some(trusted_comment.as_str()),
Some("signature from tauri secret key"),
)
.context("failed to sign file")?;
let encoded_signature =
base64::engine::general_purpose::STANDARD.encode(signature_box.to_string());
std::fs::write(&signature_path, encoded_signature.as_bytes())
.fs_context("failed to write signature file", signature_path.clone())?;
Ok((
fs::canonicalize(&signature_path)
.fs_context("failed to canonicalize signature file", &signature_path)?,
signature_box,
))
}
/// Gets the updater secret key from the given private key and password.
///
/// If `password` is `None`, a password is going to be prompted interactively.
pub fn secret_key<S: AsRef<[u8]>>(
private_key: S,
password: Option<String>,
) -> crate::Result<SecretKey> {
let decoded_secret = decode_key(private_key).context("failed to decode base64 secret key")?;
let sk_box =
SecretKeyBox::from_string(&decoded_secret).context("failed to load updater private key")?;
let sk = sk_box
.into_secret_key(password)
.context("incorrect updater private key password")?;
Ok(sk)
}
/// Gets the updater secret key from the given private key and password.
pub fn pub_key<S: AsRef<[u8]>>(public_key: S) -> crate::Result<PublicKey> {
let decoded_publick = decode_key(public_key).context("failed to decode base64 pubkey")?;
let pk_box =
PublicKeyBox::from_string(&decoded_publick).context("failed to load updater pubkey")?;
let pk = pk_box
.into_public_key()
.context("failed to convert updater pubkey")?;
Ok(pk)
}
fn unix_timestamp() -> u64 {
let start = SystemTime::now();
let since_the_epoch = start
.duration_since(UNIX_EPOCH)
.expect("system clock is incorrect");
since_the_epoch.as_secs()
}
fn open_data_file<P>(data_path: P) -> crate::Result<BufReader<File>>
where
P: AsRef<Path>,
{
let data_path = data_path.as_ref();
let file = OpenOptions::new()
.read(true)
.open(data_path)
.fs_context("failed to open data file", data_path.to_path_buf())?;
Ok(BufReader::new(file))
}
#[cfg(test)]
mod tests {
use super::*;
// This was encrypted with an empty string
const PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5dkpDN09RZm5GeVAzc2RuYlNzWVVJelJRQnNIV2JUcGVXZUplWXZXYXpqUUFBQkFBQUFBQUFBQUFBQUlBQUFBQTZrN2RnWGh5dURxSzZiL1ZQSDdNcktiaHRxczQwMXdQelRHbjRNcGVlY1BLMTBxR2dpa3I3dDE1UTVDRDE4MXR4WlQwa1BQaXdxKy9UU2J2QmVSNXhOQWFDeG1GSVllbUNpTGJQRkhhTnROR3I5RmdUZi90OGtvaGhJS1ZTcjdZU0NyYzhQWlQ5cGM9Cg==";
// minisign >=0.7.4,<0.8.0 couldn't handle empty passwords if the private key is encrypted with an empty string.
#[test]
fn empty_password_is_valid() {
let path = std::env::temp_dir().join("minisign-password-text.txt");
std::fs::write(&path, b"TAURI").expect("failed to write test file");
let secret_key =
secret_key(PRIVATE_KEY, Some("".into())).expect("failed to resolve secret key");
sign_file(&secret_key, &path, None).expect("failed to sign file");
}
#[test]
fn embeds_version_in_trusted_comment() {
let path = std::env::temp_dir().join("minisign-versioned-text.txt");
std::fs::write(&path, b"TAURI").expect("failed to write test file");
let secret_key =
secret_key(PRIVATE_KEY, Some("".into())).expect("failed to resolve secret key");
let (_, signature) = sign_file(&secret_key, &path, Some("1.2.3")).expect("failed to sign file");
let trusted_comment = signature
.trusted_comment()
.expect("failed to read trusted comment");
assert!(
trusted_comment.ends_with("\tversion:1.2.3"),
"unexpected trusted comment: {trusted_comment}"
);
// the historical prefix must stay intact so older consumers keep parsing it
assert!(trusted_comment.starts_with("timestamp:"));
assert!(trusted_comment.contains("\tfile:minisign-versioned-text.txt\t"));
let (_, signature) = sign_file(&secret_key, &path, None).expect("failed to sign file");
assert!(
!signature
.trusted_comment()
.expect("failed to read trusted comment")
.contains("version:")
);
}
#[test]
fn rejects_version_that_breaks_the_trusted_comment() {
let path = std::env::temp_dir().join("minisign-invalid-version-text.txt");
std::fs::write(&path, b"TAURI").expect("failed to write test file");
let secret_key =
secret_key(PRIVATE_KEY, Some("".into())).expect("failed to resolve secret key");
assert!(sign_file(&secret_key, &path, Some("1.0.0\ttampered")).is_err());
assert!(sign_file(&secret_key, &path, Some("1.0.0\ntampered")).is_err());
}
// This tests the newly generated keys with empty string password works
// minisign >=0.7.4,<=0.8.0 generate keys unencrypted if the password is empty but is marked encrypted hence unusable
#[test]
fn generate_empty_password_keys_and_use() {
let KeyPair { pk, sk } = generate_key(Some("".to_owned())).unwrap();
let pk = pub_key(pk).unwrap();
let sk = secret_key(sk, Some("".into())).unwrap();
let data = b"TAURI".as_slice();
sign(Some(&pk), &sk, data, None, None).expect("failed to sign file");
}
}