use super::{AppSettings, DevProcess, ExitReason, Options, RustAppSettings, Target};
use crate::CommandExt;
use anyhow::Context;
#[cfg(target_os = "linux")]
use heck::ToKebabCase;
use shared_child::SharedChild;
use std::{
fs::rename,
io::{BufReader, ErrorKind, Write},
path::{Path, PathBuf},
process::{Command, ExitStatus, Stdio},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex,
},
};
pub struct DevChild {
manually_killed_app: Arc<AtomicBool>,
build_child: Option<Arc<SharedChild>>,
app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
}
impl DevProcess for DevChild {
fn kill(&self) -> std::io::Result<()> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.kill()?;
} else if let Some(child) = &self.build_child {
child.kill()?;
}
self.manually_killed_app.store(true, Ordering::Relaxed);
Ok(())
}
fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.try_wait()
} else if let Some(child) = &self.build_child {
child.try_wait()
} else {
unreachable!()
}
}
fn wait(&self) -> std::io::Result<ExitStatus> {
if let Some(child) = &*self.app_child.lock().unwrap() {
child.wait()
} else if let Some(child) = &self.build_child {
child.wait()
} else {
unreachable!()
}
}
fn manually_killed_process(&self) -> bool {
self.manually_killed_app.load(Ordering::Relaxed)
}
fn is_building_app(&self) -> bool {
self.app_child.lock().unwrap().is_none()
}
}
pub fn run_dev<F: Fn(ExitStatus, ExitReason) + Send + Sync + 'static>(
options: Options,
run_args: Vec<String>,
available_targets: &mut Option<Vec<Target>>,
config_features: Vec<String>,
app_settings: &RustAppSettings,
product_name: Option<String>,
on_exit: F,
) -> crate::Result<impl DevProcess> {
let bin_path = app_settings.app_binary_path(&options)?;
let manually_killed_app = Arc::new(AtomicBool::default());
let manually_killed_app_ = manually_killed_app.clone();
let app_child = Arc::new(Mutex::new(None));
let app_child_ = app_child.clone();
let build_child = build_dev_app(
options,
available_targets,
config_features,
move |status, reason| {
if status.success() {
let bin_path =
rename_app(&bin_path, product_name.as_deref()).expect("failed to rename app");
let mut app = Command::new(bin_path);
app.stdout(os_pipe::dup_stdout().unwrap());
app.stderr(os_pipe::dup_stderr().unwrap());
app.args(run_args);
let app_child = Arc::new(SharedChild::spawn(&mut app).unwrap());
crate::dev::wait_dev_process(
DevChild {
manually_killed_app: manually_killed_app_,
build_child: None,
app_child: Arc::new(Mutex::new(Some(app_child.clone()))),
},
on_exit,
);
app_child_.lock().unwrap().replace(app_child);
} else {
on_exit(
status,
if manually_killed_app_.load(Ordering::Relaxed) {
ExitReason::TriggeredKill
} else {
reason
},
);
}
},
)?;
Ok(DevChild {
manually_killed_app,
build_child: Some(build_child),
app_child,
})
}
pub fn build(
options: Options,
app_settings: &RustAppSettings,
product_name: Option<String>,
available_targets: &mut Option<Vec<Target>>,
config_features: Vec<String>,
) -> crate::Result<()> {
let bin_path = app_settings.app_binary_path(&options)?;
let out_dir = bin_path.parent().unwrap();
let bin_name = bin_path.file_stem().unwrap();
if !std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "false") {
std::env::set_var("STATIC_VCRUNTIME", "true");
}
if options.target == Some("universal-apple-darwin".into()) {
std::fs::create_dir_all(out_dir).with_context(|| "failed to create project out directory")?;
let mut lipo_cmd = Command::new("lipo");
lipo_cmd
.arg("-create")
.arg("-output")
.arg(out_dir.join(bin_name));
for triple in ["aarch64-apple-darwin", "x86_64-apple-darwin"] {
let mut options = options.clone();
options.target.replace(triple.into());
let triple_out_dir = app_settings
.out_dir(Some(triple.into()), options.debug)
.with_context(|| format!("failed to get {} out dir", triple))?;
build_production_app(options, available_targets, config_features.clone())
.with_context(|| format!("failed to build {} binary", triple))?;
lipo_cmd.arg(triple_out_dir.join(bin_name));
}
let lipo_status = lipo_cmd.output_ok()?.status;
if !lipo_status.success() {
return Err(anyhow::anyhow!(format!(
"Result of `lipo` command was unsuccessful: {}. (Is `lipo` installed?)",
lipo_status
)));
}
} else {
build_production_app(options, available_targets, config_features)
.with_context(|| "failed to build app")?;
}
rename_app(&bin_path, product_name.as_deref())?;
Ok(())
}
fn build_dev_app<F: FnOnce(ExitStatus, ExitReason) + Send + 'static>(
options: Options,
available_targets: &mut Option<Vec<Target>>,
config_features: Vec<String>,
on_exit: F,
) -> crate::Result<Arc<SharedChild>> {
let mut build_cmd = build_command(options, available_targets, config_features)?;
let runner = build_cmd.get_program().to_string_lossy().into_owned();
build_cmd
.env(
"CARGO_TERM_PROGRESS_WIDTH",
terminal::stderr_width()
.map(|width| {
if cfg!(windows) {
std::cmp::min(60, width)
} else {
width
}
})
.unwrap_or(if cfg!(windows) { 60 } else { 80 })
.to_string(),
)
.env("CARGO_TERM_PROGRESS_WHEN", "always");
build_cmd.arg("--color");
build_cmd.arg("always");
build_cmd.stdout(os_pipe::dup_stdout()?);
build_cmd.stderr(Stdio::piped());
let build_child = match SharedChild::spawn(&mut build_cmd) {
Ok(c) => Ok(c),
Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
"`{}` command not found.{}",
runner,
if runner == "cargo" {
" Please follow the Tauri setup guide: https://tauri.app/v1/guides/getting-started/prerequisites"
} else {
""
}
)),
Err(e) => Err(e.into()),
}?;
let build_child = Arc::new(build_child);
let build_child_stderr = build_child.take_stderr().unwrap();
let mut stderr = BufReader::new(build_child_stderr);
let stderr_lines = Arc::new(Mutex::new(Vec::new()));
let stderr_lines_ = stderr_lines.clone();
std::thread::spawn(move || {
let mut buf = Vec::new();
let mut lines = stderr_lines_.lock().unwrap();
let mut io_stderr = std::io::stderr();
loop {
buf.clear();
match tauri_utils::io::read_line(&mut stderr, &mut buf) {
Ok(s) if s == 0 => break,
_ => (),
}
let _ = io_stderr.write_all(&buf);
if !buf.ends_with(&[b'\r']) {
let _ = io_stderr.write_all(b"\n");
}
lines.push(String::from_utf8_lossy(&buf).into_owned());
}
});
let build_child_ = build_child.clone();
std::thread::spawn(move || {
let status = build_child_.wait().expect("failed to wait on build");
if status.success() {
on_exit(status, ExitReason::NormalExit);
} else {
let is_cargo_compile_error = stderr_lines
.lock()
.unwrap()
.last()
.map(|l| l.contains("could not compile"))
.unwrap_or_default();
stderr_lines.lock().unwrap().clear();
on_exit(
status,
if status.code() == Some(101) && is_cargo_compile_error {
ExitReason::CompilationFailed
} else {
ExitReason::NormalExit
},
);
}
});
Ok(build_child)
}
fn build_production_app(
options: Options,
available_targets: &mut Option<Vec<Target>>,
config_features: Vec<String>,
) -> crate::Result<()> {
let mut build_cmd = build_command(options, available_targets, config_features)?;
let runner = build_cmd.get_program().to_string_lossy().into_owned();
match build_cmd.piped() {
Ok(status) if status.success() => Ok(()),
Ok(_) => Err(anyhow::anyhow!("failed to build app")),
Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
"`{}` command not found.{}",
runner,
if runner == "cargo" {
" Please follow the Tauri setup guide: https://tauri.app/v1/guides/getting-started/prerequisites"
} else {
""
}
)),
Err(e) => Err(e.into()),
}
}
fn build_command(
options: Options,
available_targets: &mut Option<Vec<Target>>,
config_features: Vec<String>,
) -> crate::Result<Command> {
let runner = options.runner.unwrap_or_else(|| "cargo".into());
if let Some(target) = &options.target {
if available_targets.is_none() {
*available_targets = fetch_available_targets();
}
validate_target(available_targets, target)?;
}
let mut args = Vec::new();
if !options.args.is_empty() {
args.extend(options.args);
}
let mut features = config_features;
if let Some(f) = options.features {
features.extend(f);
}
if !features.is_empty() {
args.push("--features".into());
args.push(features.join(","));
}
if !options.debug {
args.push("--release".into());
}
if let Some(target) = options.target {
args.push("--target".into());
args.push(target);
}
let mut build_cmd = Command::new(runner);
build_cmd.arg("build");
build_cmd.args(args);
Ok(build_cmd)
}
fn fetch_available_targets() -> Option<Vec<Target>> {
if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() {
let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
Some(
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(),
)
} else {
None
}
}
fn validate_target(available_targets: &Option<Vec<Target>>, target: &str) -> crate::Result<()> {
if let Some(available_targets) = 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 rename_app(bin_path: &Path, product_name: Option<&str>) -> crate::Result<PathBuf> {
if let Some(product_name) = product_name {
#[cfg(target_os = "linux")]
let product_name = product_name.to_kebab_case();
let product_path = bin_path
.parent()
.unwrap()
.join(product_name)
.with_extension(bin_path.extension().unwrap_or_default());
rename(bin_path, &product_path).with_context(|| {
format!(
"failed to rename `{}` to `{}`",
bin_path.display(),
product_path.display(),
)
})?;
Ok(product_path)
} else {
Ok(bin_path.to_path_buf())
}
}
#[cfg(unix)]
mod terminal {
use std::mem;
pub fn stderr_width() -> Option<usize> {
unsafe {
let mut winsize: libc::winsize = mem::zeroed();
#[allow(clippy::useless_conversion)]
if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
return None;
}
if winsize.ws_col > 0 {
Some(winsize.ws_col as usize)
} else {
None
}
}
}
}
#[cfg(windows)]
mod terminal {
use std::{cmp, mem, ptr};
use winapi::um::fileapi::*;
use winapi::um::handleapi::*;
use winapi::um::processenv::*;
use winapi::um::winbase::*;
use winapi::um::wincon::*;
use winapi::um::winnt::*;
pub fn stderr_width() -> Option<usize> {
unsafe {
let stdout = GetStdHandle(STD_ERROR_HANDLE);
let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
}
let h = CreateFileA(
"CONOUT$\0".as_ptr() as *const CHAR,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
);
if h == INVALID_HANDLE_VALUE {
return None;
}
let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
CloseHandle(h);
if rc != 0 {
let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
return Some(cmp::min(60, width));
}
None
}
}
}