diff --git a/tooling/cli/src/build.rs b/tooling/cli/src/build.rs index f137bbaf7..5c720b6e5 100644 --- a/tooling/cli/src/build.rs +++ b/tooling/cli/src/build.rs @@ -7,7 +7,6 @@ use crate::{ app_paths::{app_dir, tauri_dir}, command_env, config::{get as get_config, AppUrl, WindowUrl}, - manifest::rewrite_manifest, updater_signature::sign_file_from_env_variables, }, interface::{AppInterface, AppSettings, Interface}, @@ -55,7 +54,7 @@ pub struct Options { } pub fn command(mut options: Options) -> Result<()> { - let merge_config = if let Some(config) = &options.config { + options.config = if let Some(config) = &options.config { Some(if config.starts_with('{') { config.to_string() } else { @@ -68,9 +67,7 @@ pub fn command(mut options: Options) -> Result<()> { let tauri_path = tauri_dir(); set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?; - let config = get_config(merge_config.as_deref())?; - - let manifest = rewrite_manifest(config.clone())?; + let config = get_config(options.config.as_deref())?; let config_guard = config.lock().unwrap(); let config_ = config_guard.as_ref().unwrap(); @@ -210,7 +207,7 @@ pub fn command(mut options: Options) -> Result<()> { } let settings = app_settings - .get_bundler_settings(&options.into(), &manifest, config_, out_dir, package_types) + .get_bundler_settings(&options.into(), config_, out_dir, package_types) .with_context(|| "failed to build bundler settings")?; // set env vars used by the bundler diff --git a/tooling/cli/src/dev.rs b/tooling/cli/src/dev.rs index 099581400..e2e9b84a4 100644 --- a/tooling/cli/src/dev.rs +++ b/tooling/cli/src/dev.rs @@ -6,33 +6,26 @@ use crate::{ helpers::{ app_paths::{app_dir, tauri_dir}, command_env, - config::{get as get_config, reload as reload_config, AppUrl, ConfigHandle, WindowUrl}, - manifest::{rewrite_manifest, Manifest}, + config::{get as get_config, AppUrl, WindowUrl}, }, - interface::{AppInterface, DevProcess, ExitReason, Interface}, + interface::{AppInterface, ExitReason, Interface}, Result, }; use clap::Parser; use anyhow::Context; use log::{error, info, warn}; -use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use once_cell::sync::OnceCell; use shared_child::SharedChild; use std::{ env::set_current_dir, - ffi::OsStr, - fs::FileType, io::Write, - path::{Path, PathBuf}, process::{exit, Command, ExitStatus, Stdio}, sync::{ atomic::{AtomicBool, Ordering}, - mpsc::channel, Arc, Mutex, }, - time::Duration, }; static BEFORE_DEV: OnceCell>> = OnceCell::new(); @@ -41,7 +34,7 @@ static KILL_BEFORE_DEV_FLAG: OnceCell = OnceCell::new(); #[cfg(unix)] const KILL_CHILDREN_SCRIPT: &[u8] = include_bytes!("../scripts/kill-children.sh"); -const TAURI_DEV_WATCHER_GITIGNORE: &[u8] = include_bytes!("../tauri-dev-watcher.gitignore"); +pub const TAURI_DEV_WATCHER_GITIGNORE: &[u8] = include_bytes!("../tauri-dev-watcher.gitignore"); #[derive(Debug, Clone, Parser)] #[clap(about = "Tauri dev", trailing_var_arg(true))] @@ -60,7 +53,7 @@ pub struct Options { exit_on_panic: bool, /// JSON string or path to JSON file to merge with tauri.conf.json #[clap(short, long)] - config: Option, + pub config: Option, /// Run the code in release mode #[clap(long = "release")] pub release_mode: bool, @@ -80,7 +73,7 @@ pub fn command(options: Options) -> Result<()> { fn command_internal(mut options: Options) -> Result<()> { let tauri_path = tauri_dir(); - let merge_config = if let Some(config) = &options.config { + options.config = if let Some(config) = &options.config { Some(if config.starts_with('{') { config.to_string() } else { @@ -92,7 +85,7 @@ fn command_internal(mut options: Options) -> Result<()> { set_current_dir(&tauri_path).with_context(|| "failed to change current working directory")?; - let config = get_config(merge_config.as_deref())?; + let config = get_config(options.config.as_deref())?; if let Some(before_dev) = &config .lock() @@ -167,19 +160,6 @@ fn command_internal(mut options: Options) -> Result<()> { .clone(); } - let manifest = { - let (tx, rx) = channel(); - let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); - watcher.watch(tauri_path.join("Cargo.toml"), RecursiveMode::Recursive)?; - let manifest = rewrite_manifest(config.clone())?; - loop { - if let Ok(DebouncedEvent::NoticeWrite(_)) = rx.recv() { - break; - } - } - manifest - }; - let mut cargo_features = config .lock() .unwrap() @@ -255,29 +235,9 @@ fn command_internal(mut options: Options) -> Result<()> { let mut interface = AppInterface::new(config.lock().unwrap().as_ref().unwrap())?; let exit_on_panic = options.exit_on_panic; - let process = interface.dev(options.clone().into(), &manifest, move |status, reason| { + interface.dev(options.into(), move |status, reason| { on_dev_exit(status, reason, exit_on_panic) - })?; - let shared_process = Arc::new(Mutex::new(process)); - - if let Err(e) = watch( - interface, - shared_process.clone(), - tauri_path, - merge_config, - config, - options, - manifest, - ) { - shared_process - .lock() - .unwrap() - .kill() - .with_context(|| "failed to kill app process")?; - Err(e) - } else { - Ok(()) - } + }) } fn on_dev_exit(status: ExitStatus, reason: ExitReason, exit_on_panic: bool) { @@ -309,92 +269,6 @@ fn check_for_updates() -> Result<()> { Ok(()) } -fn lookup(dir: &Path, mut f: F) { - let mut default_gitignore = std::env::temp_dir(); - default_gitignore.push(".tauri-dev"); - let _ = std::fs::create_dir_all(&default_gitignore); - default_gitignore.push(".gitignore"); - if !default_gitignore.exists() { - if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) { - let _ = file.write_all(TAURI_DEV_WATCHER_GITIGNORE); - } - } - - let mut builder = ignore::WalkBuilder::new(dir); - let _ = builder.add_ignore(default_gitignore); - if let Ok(ignore_file) = std::env::var("TAURI_DEV_WATCHER_IGNORE_FILE") { - builder.add_ignore(ignore_file); - } - builder.require_git(false).ignore(false).max_depth(Some(1)); - - for entry in builder.build().flatten() { - f(entry.file_type().unwrap(), dir.join(entry.path())); - } -} - -#[allow(clippy::too_many_arguments)] -fn watch>( - mut interface: I, - process: Arc>, - tauri_path: PathBuf, - merge_config: Option, - config: ConfigHandle, - options: Options, - mut manifest: Manifest, -) -> Result<()> { - let (tx, rx) = channel(); - - let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); - lookup(&tauri_path, |file_type, path| { - if path != tauri_path { - let _ = watcher.watch( - path, - if file_type.is_dir() { - RecursiveMode::Recursive - } else { - RecursiveMode::NonRecursive - }, - ); - } - }); - - let exit_on_panic = options.exit_on_panic; - - loop { - if let Ok(event) = rx.recv() { - let event_path = match event { - DebouncedEvent::Create(path) => Some(path), - DebouncedEvent::Remove(path) => Some(path), - DebouncedEvent::Rename(_, dest) => Some(dest), - DebouncedEvent::Write(path) => Some(path), - _ => None, - }; - - if let Some(event_path) = event_path { - if event_path.file_name() == Some(OsStr::new("tauri.conf.json")) { - reload_config(merge_config.as_deref())?; - manifest = rewrite_manifest(config.clone())?; - } else { - // When tauri.conf.json is changed, rewrite_manifest will be called - // which will trigger the watcher again - // So the app should only be started when a file other than tauri.conf.json is changed - let mut p = process.lock().unwrap(); - p.kill().with_context(|| "failed to kill app process")?; - // wait for the process to exit - loop { - if let Ok(Some(_)) = p.try_wait() { - break; - } - } - *p = interface.dev(options.clone().into(), &manifest, move |status, reason| { - on_dev_exit(status, reason, exit_on_panic) - })?; - } - } - } - } -} - fn kill_before_dev_process() { if let Some(child) = BEFORE_DEV.get() { let child = child.lock().unwrap(); diff --git a/tooling/cli/src/helpers/config.rs b/tooling/cli/src/helpers/config.rs index 64614d9db..723056edb 100644 --- a/tooling/cli/src/helpers/config.rs +++ b/tooling/cli/src/helpers/config.rs @@ -102,7 +102,6 @@ pub fn get(merge_config: Option<&str>) -> crate::Result { get_internal(merge_config, false) } -pub fn reload(merge_config: Option<&str>) -> crate::Result<()> { - get_internal(merge_config, true)?; - Ok(()) +pub fn reload(merge_config: Option<&str>) -> crate::Result { + get_internal(merge_config, true) } diff --git a/tooling/cli/src/helpers/mod.rs b/tooling/cli/src/helpers/mod.rs index 223b17cb8..5284a6ec5 100644 --- a/tooling/cli/src/helpers/mod.rs +++ b/tooling/cli/src/helpers/mod.rs @@ -5,7 +5,6 @@ pub mod app_paths; pub mod config; pub mod framework; -pub mod manifest; pub mod template; pub mod updater_signature; diff --git a/tooling/cli/src/interface/mod.rs b/tooling/cli/src/interface/mod.rs index c14165623..6133531be 100644 --- a/tooling/cli/src/interface/mod.rs +++ b/tooling/cli/src/interface/mod.rs @@ -9,7 +9,7 @@ use std::{ process::ExitStatus, }; -use crate::helpers::{config::Config, manifest::Manifest}; +use crate::helpers::config::Config; use tauri_bundler::bundle::{PackageType, Settings, SettingsBuilder}; pub use rust::{Options, Rust as AppInterface}; @@ -19,7 +19,6 @@ pub trait AppSettings { fn get_bundle_settings( &self, config: &Config, - manifest: &Manifest, features: &[String], ) -> crate::Result; fn app_binary_path(&self, options: &Options) -> crate::Result; @@ -32,7 +31,6 @@ pub trait AppSettings { fn get_bundler_settings( &self, options: &Options, - manifest: &Manifest, config: &Config, out_dir: &Path, package_types: Option>, @@ -51,7 +49,7 @@ pub trait AppSettings { let mut settings_builder = SettingsBuilder::new() .package_settings(self.get_package_settings()) - .bundle_settings(self.get_bundle_settings(config, manifest, &enabled_features)?) + .bundle_settings(self.get_bundle_settings(config, &enabled_features)?) .binaries(self.get_binaries(config, &target)?) .project_out_directory(out_dir) .target(target); @@ -64,11 +62,6 @@ pub trait AppSettings { } } -pub trait DevProcess { - fn kill(&self) -> std::io::Result<()>; - fn try_wait(&self) -> std::io::Result>; -} - #[derive(Debug)] pub enum ExitReason { /// Killed manually. @@ -81,15 +74,13 @@ pub enum ExitReason { pub trait Interface: Sized { type AppSettings: AppSettings; - type Dev: DevProcess; fn new(config: &Config) -> crate::Result; fn app_settings(&self) -> &Self::AppSettings; fn build(&mut self, options: Options) -> crate::Result<()>; - fn dev( + fn dev( &mut self, options: Options, - manifest: &Manifest, on_exit: F, - ) -> crate::Result; + ) -> crate::Result<()>; } diff --git a/tooling/cli/src/interface/rust.rs b/tooling/cli/src/interface/rust.rs index f64214d3d..c98cf191f 100644 --- a/tooling/cli/src/interface/rust.rs +++ b/tooling/cli/src/interface/rust.rs @@ -3,21 +3,25 @@ // SPDX-License-Identifier: MIT use std::{ - fs::{rename, File}, + ffi::OsStr, + fs::{rename, File, FileType}, io::{BufReader, ErrorKind, Read, Write}, path::{Path, PathBuf}, process::{Command, ExitStatus, Stdio}, str::FromStr, sync::{ atomic::{AtomicBool, Ordering}, + mpsc::channel, Arc, Mutex, }, + time::Duration, }; use anyhow::Context; #[cfg(target_os = "linux")] use heck::ToKebabCase; use log::warn; +use notify::{watcher, DebouncedEvent, RecursiveMode, Watcher}; use serde::Deserialize; use shared_child::SharedChild; use tauri_bundler::{ @@ -25,16 +29,18 @@ use tauri_bundler::{ UpdaterSettings, WindowsSettings, }; -use super::{AppSettings, DevProcess, ExitReason, Interface}; +use super::{AppSettings, ExitReason, Interface}; use crate::{ helpers::{ app_paths::tauri_dir, - config::{wix_settings, Config}, - manifest::Manifest, + config::{reload as reload_config, wix_settings, Config}, }, CommandExt, }; +mod manifest; +use manifest::{rewrite_manifest, Manifest}; + #[derive(Debug, Clone)] pub struct Options { pub runner: Option, @@ -42,6 +48,7 @@ pub struct Options { pub target: Option, pub features: Option>, pub args: Vec, + pub config: Option, } impl From for Options { @@ -52,6 +59,7 @@ impl From for Options { target: options.target, features: options.features, args: options.args, + config: options.config, } } } @@ -64,6 +72,7 @@ impl From for Options { target: options.target, features: options.features, args: options.args, + config: options.config, } } } @@ -74,7 +83,7 @@ pub struct DevChild { app_child: Arc>>>, } -impl DevProcess for DevChild { +impl DevChild { fn kill(&self) -> std::io::Result<()> { if let Some(child) = &*self.app_child.lock().unwrap() { child.kill()?; @@ -109,11 +118,22 @@ pub struct Rust { impl Interface for Rust { type AppSettings = RustAppSettings; - type Dev = DevChild; fn new(config: &Config) -> crate::Result { + let manifest = { + let (tx, rx) = channel(); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + watcher.watch(tauri_dir().join("Cargo.toml"), RecursiveMode::Recursive)?; + let manifest = rewrite_manifest(config)?; + loop { + if let Ok(DebouncedEvent::NoticeWrite(_)) = rx.recv() { + break; + } + } + manifest + }; Ok(Self { - app_settings: RustAppSettings::new(config)?, + app_settings: RustAppSettings::new(config, manifest)?, config_features: config.build.features.clone().unwrap_or_default(), product_name: config.package.product_name.clone(), available_targets: None, @@ -172,12 +192,87 @@ impl Interface for Rust { Ok(()) } - fn dev( + fn dev( &mut self, options: Options, - manifest: &Manifest, on_exit: F, - ) -> crate::Result { + ) -> crate::Result<()> { + let on_exit = Arc::new(on_exit); + + let on_exit_ = on_exit.clone(); + let child = self.run_dev(options.clone(), move |status, reason| { + on_exit_(status, reason) + })?; + + self.run_dev_watcher(child, options, on_exit) + } +} + +fn lookup(dir: &Path, mut f: F) { + let mut default_gitignore = std::env::temp_dir(); + default_gitignore.push(".tauri-dev"); + let _ = std::fs::create_dir_all(&default_gitignore); + default_gitignore.push(".gitignore"); + if !default_gitignore.exists() { + if let Ok(mut file) = std::fs::File::create(default_gitignore.clone()) { + let _ = file.write_all(crate::dev::TAURI_DEV_WATCHER_GITIGNORE); + } + } + + let mut builder = ignore::WalkBuilder::new(dir); + let _ = builder.add_ignore(default_gitignore); + if let Ok(ignore_file) = std::env::var("TAURI_DEV_WATCHER_IGNORE_FILE") { + builder.add_ignore(ignore_file); + } + builder.require_git(false).ignore(false).max_depth(Some(1)); + + for entry in builder.build().flatten() { + f(entry.file_type().unwrap(), dir.join(entry.path())); + } +} + +impl Rust { + fn fetch_available_targets(&mut self) { + if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() { + let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); + self.available_targets.replace( + stdout + .split('\n') + .map(|t| { + let mut s = t.split(' '); + let name = s.next().unwrap().to_string(); + let installed = s.next().map(|v| v == "(installed)").unwrap_or_default(); + Target { name, installed } + }) + .filter(|t| !t.name.is_empty()) + .collect(), + ); + } + } + + fn validate_target(&self, target: &str) -> crate::Result<()> { + if let Some(available_targets) = &self.available_targets { + if let Some(target) = available_targets.iter().find(|t| t.name == target) { + if !target.installed { + anyhow::bail!( + "Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.", + target = target.name, + installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::>().join(", ") + ); + } + } + if !available_targets.iter().any(|t| t.name == target) { + anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target); + } + } + Ok(()) + } + + fn run_dev( + &mut self, + options: Options, + on_exit: F, + ) -> crate::Result { let bin_path = self.app_settings.app_binary_path(&options)?; let product_name = self.product_name.clone(); @@ -207,7 +302,7 @@ impl Interface for Rust { build_cmd.arg("build").arg("--color").arg("always"); if !options.args.contains(&"--no-default-features".into()) { - let manifest_features = manifest.features(); + let manifest_features = self.app_settings.manifest.features(); let enable_features: Vec = manifest_features .get("default") .cloned() @@ -356,43 +451,66 @@ impl Interface for Rust { app_child, }) } -} -impl Rust { - fn fetch_available_targets(&mut self) { - if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() { - let stdout = String::from_utf8_lossy(&output.stdout).into_owned(); - self.available_targets.replace( - stdout - .split('\n') - .map(|t| { - let mut s = t.split(' '); - let name = s.next().unwrap().to_string(); - let installed = s.next().map(|v| v == "(installed)").unwrap_or_default(); - Target { name, installed } - }) - .filter(|t| !t.name.is_empty()) - .collect(), - ); - } - } + fn run_dev_watcher( + &mut self, + child: DevChild, + options: Options, + on_exit: Arc, + ) -> crate::Result<()> { + let process = Arc::new(Mutex::new(child)); + let (tx, rx) = channel(); + let tauri_path = tauri_dir(); - fn validate_target(&self, target: &str) -> crate::Result<()> { - if let Some(available_targets) = &self.available_targets { - if let Some(target) = available_targets.iter().find(|t| t.name == target) { - if !target.installed { - anyhow::bail!( - "Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.", - target = target.name, - installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::>().join(", ") - ); + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + lookup(&tauri_path, |file_type, path| { + if path != tauri_path { + let _ = watcher.watch( + path, + if file_type.is_dir() { + RecursiveMode::Recursive + } else { + RecursiveMode::NonRecursive + }, + ); + } + }); + + loop { + let on_exit = on_exit.clone(); + if let Ok(event) = rx.recv() { + let event_path = match event { + DebouncedEvent::Create(path) => Some(path), + DebouncedEvent::Remove(path) => Some(path), + DebouncedEvent::Rename(_, dest) => Some(dest), + DebouncedEvent::Write(path) => Some(path), + _ => None, + }; + + if let Some(event_path) = event_path { + if event_path.file_name() == Some(OsStr::new("tauri.conf.json")) { + let config = reload_config(options.config.as_deref())?; + self.app_settings.manifest = + rewrite_manifest(config.lock().unwrap().as_ref().unwrap())?; + } else { + // When tauri.conf.json is changed, rewrite_manifest will be called + // which will trigger the watcher again + // So the app should only be started when a file other than tauri.conf.json is changed + let mut p = process.lock().unwrap(); + p.kill().with_context(|| "failed to kill app process")?; + // wait for the process to exit + loop { + if let Ok(Some(_)) = p.try_wait() { + break; + } + } + *p = self.run_dev(options.clone(), move |status, reason| { + on_exit(status, reason) + })?; + } } } - if !available_targets.iter().any(|t| t.name == target) { - anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target); - } } - Ok(()) } fn build_app(&mut self, options: Options) -> crate::Result<()> { @@ -533,6 +651,7 @@ struct CargoConfig { } pub struct RustAppSettings { + manifest: Manifest, cargo_settings: CargoSettings, cargo_package_settings: CargoPackageSettings, package_settings: PackageSettings, @@ -546,11 +665,10 @@ impl AppSettings for RustAppSettings { fn get_bundle_settings( &self, config: &Config, - manifest: &Manifest, features: &[String], ) -> crate::Result { tauri_config_to_bundle_settings( - manifest, + &self.manifest, features, config.tauri.bundle.clone(), config.tauri.system_tray.clone(), @@ -690,7 +808,7 @@ impl AppSettings for RustAppSettings { } impl RustAppSettings { - pub fn new(config: &Config) -> crate::Result { + pub fn new(config: &Config, manifest: Manifest) -> crate::Result { let cargo_settings = CargoSettings::load(&tauri_dir()).with_context(|| "failed to load cargo settings")?; let cargo_package_settings = match &cargo_settings.package { @@ -725,6 +843,7 @@ impl RustAppSettings { }; Ok(Self { + manifest, cargo_settings, cargo_package_settings, package_settings, diff --git a/tooling/cli/src/helpers/manifest.rs b/tooling/cli/src/interface/rust/manifest.rs similarity index 95% rename from tooling/cli/src/helpers/manifest.rs rename to tooling/cli/src/interface/rust/manifest.rs index b29196010..d19ad8370 100644 --- a/tooling/cli/src/helpers/manifest.rs +++ b/tooling/cli/src/interface/rust/manifest.rs @@ -2,9 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use super::{ +use crate::helpers::{ app_paths::tauri_dir, - config::{ConfigHandle, PatternKind}, + config::{Config, PatternKind}, }; use anyhow::Context; @@ -184,13 +184,10 @@ fn write_features( } } -pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { +pub fn rewrite_manifest(config: &Config) -> crate::Result { let manifest_path = tauri_dir().join("Cargo.toml"); let mut manifest = read_manifest(&manifest_path)?; - let config_guard = config.lock().unwrap(); - let config = config_guard.as_ref().unwrap(); - let mut tauri_build_features = HashSet::new(); if let PatternKind::Isolation { .. } = config.tauri.pattern { tauri_build_features.insert("isolation".to_string()); @@ -209,7 +206,7 @@ pub fn rewrite_manifest(config: ConfigHandle) -> crate::Result { let mut tauri_features = HashSet::from_iter(config.tauri.features().into_iter().map(|f| f.to_string())); - let cli_managed_tauri_features = super::config::TauriConfig::all_features(); + let cli_managed_tauri_features = crate::helpers::config::TauriConfig::all_features(); let res = match write_features( manifest .as_table_mut()