|
@@ -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<String>,
|
|
@@ -42,6 +48,7 @@ pub struct Options {
|
|
|
pub target: Option<String>,
|
|
|
pub features: Option<Vec<String>>,
|
|
|
pub args: Vec<String>,
|
|
|
+ pub config: Option<String>,
|
|
|
}
|
|
|
|
|
|
impl From<crate::build::Options> for Options {
|
|
@@ -52,6 +59,7 @@ impl From<crate::build::Options> for Options {
|
|
|
target: options.target,
|
|
|
features: options.features,
|
|
|
args: options.args,
|
|
|
+ config: options.config,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -64,6 +72,7 @@ impl From<crate::dev::Options> for Options {
|
|
|
target: options.target,
|
|
|
features: options.features,
|
|
|
args: options.args,
|
|
|
+ config: options.config,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
@@ -74,7 +83,7 @@ pub struct DevChild {
|
|
|
app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
|
|
|
}
|
|
|
|
|
|
-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<Self> {
|
|
|
+ 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<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
|
|
|
+ fn dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
|
|
|
+ &mut self,
|
|
|
+ options: Options,
|
|
|
+ on_exit: F,
|
|
|
+ ) -> 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<F: FnMut(FileType, PathBuf)>(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::<Vec<&str>>().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<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
|
|
|
&mut self,
|
|
|
options: Options,
|
|
|
- manifest: &Manifest,
|
|
|
on_exit: F,
|
|
|
- ) -> crate::Result<Self::Dev> {
|
|
|
+ ) -> crate::Result<DevChild> {
|
|
|
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<String> = 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<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
|
|
|
+ &mut self,
|
|
|
+ child: DevChild,
|
|
|
+ options: Options,
|
|
|
+ on_exit: Arc<F>,
|
|
|
+ ) -> crate::Result<()> {
|
|
|
+ let process = Arc::new(Mutex::new(child));
|
|
|
+ let (tx, rx) = channel();
|
|
|
+ let tauri_path = tauri_dir();
|
|
|
+
|
|
|
+ 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
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }
|
|
|
+ });
|
|
|
|
|
|
- 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::<Vec<&str>>().join(", ")
|
|
|
- );
|
|
|
+ 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<BundleSettings> {
|
|
|
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<Self> {
|
|
|
+ pub fn new(config: &Config, manifest: Manifest) -> crate::Result<Self> {
|
|
|
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,
|