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:
167
crates/tauri-cli/src/helpers/app_paths.rs
Normal file
167
crates/tauri-cli/src/helpers/app_paths.rs
Normal 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())
|
||||
}
|
||||
99
crates/tauri-cli/src/helpers/cargo.rs
Normal file
99
crates/tauri-cli/src/helpers/cargo.rs
Normal 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(())
|
||||
}
|
||||
209
crates/tauri-cli/src/helpers/cargo_manifest.rs
Normal file
209
crates/tauri-cli/src/helpers/cargo_manifest.rs
Normal 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
|
||||
}
|
||||
362
crates/tauri-cli/src/helpers/config.rs
Normal file
362
crates/tauri-cli/src/helpers/config.rs
Normal 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"
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
374
crates/tauri-cli/src/helpers/flock.rs
Normal file
374
crates/tauri-cli/src/helpers/flock.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
131
crates/tauri-cli/src/helpers/framework.rs
Normal file
131
crates/tauri-cli/src/helpers/framework.rs
Normal 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)
|
||||
}
|
||||
55
crates/tauri-cli/src/helpers/fs.rs
Normal file
55
crates/tauri-cli/src/helpers/fs.rs
Normal 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
|
||||
)
|
||||
}
|
||||
26
crates/tauri-cli/src/helpers/http.rs
Normal file
26
crates/tauri-cli/src/helpers/http.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use ureq::{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()
|
||||
}
|
||||
52
crates/tauri-cli/src/helpers/icns.json
Normal file
52
crates/tauri-cli/src/helpers/icns.json
Normal 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"
|
||||
}
|
||||
}
|
||||
152
crates/tauri-cli/src/helpers/mod.rs
Normal file
152
crates/tauri-cli/src/helpers/mod.rs
Normal 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(())
|
||||
}
|
||||
459
crates/tauri-cli/src/helpers/npm.rs
Normal file
459
crates/tauri-cli/src/helpers/npm.rs
Normal 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)
|
||||
}
|
||||
327
crates/tauri-cli/src/helpers/pbxproj.rs
Normal file
327
crates/tauri-cli/src/helpers/pbxproj.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
42
crates/tauri-cli/src/helpers/plist.rs
Normal file
42
crates/tauri-cli/src/helpers/plist.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::error::Context;
|
||||
|
||||
pub enum PlistKind {
|
||||
Path(PathBuf),
|
||||
Plist(plist::Value),
|
||||
}
|
||||
|
||||
impl From<PathBuf> for PlistKind {
|
||||
fn from(p: PathBuf) -> Self {
|
||||
Self::Path(p)
|
||||
}
|
||||
}
|
||||
impl From<plist::Value> for PlistKind {
|
||||
fn from(p: plist::Value) -> Self {
|
||||
Self::Plist(p)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_plist(src: Vec<PlistKind>) -> crate::Result<plist::Value> {
|
||||
let mut merged_plist = plist::Dictionary::new();
|
||||
|
||||
for plist_kind in src {
|
||||
let src_plist = match plist_kind {
|
||||
PlistKind::Path(p) => plist::Value::from_file(&p)
|
||||
.with_context(|| format!("failed to parse plist from {}", p.display()))?,
|
||||
PlistKind::Plist(v) => v,
|
||||
};
|
||||
if let Some(dict) = src_plist.into_dictionary() {
|
||||
for (key, value) in dict {
|
||||
merged_plist.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(plist::Value::Dictionary(merged_plist))
|
||||
}
|
||||
98
crates/tauri-cli/src/helpers/plugins.rs
Normal file
98
crates/tauri-cli/src/helpers/plugins.rs
Normal 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
61
crates/tauri-cli/src/helpers/prompts.rs
Normal file
61
crates/tauri-cli/src/helpers/prompts.rs
Normal 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")
|
||||
}
|
||||
97
crates/tauri-cli/src/helpers/template.rs
Normal file
97
crates/tauri-cli/src/helpers/template.rs
Normal 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(())
|
||||
}
|
||||
297
crates/tauri-cli/src/helpers/updater_signature.rs
Normal file
297
crates/tauri-cli/src/helpers/updater_signature.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user