desktop.rs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use super::{AppSettings, DevProcess, ExitReason, Options, RustAppSettings, RustupTarget};
  5. use crate::CommandExt;
  6. use anyhow::Context;
  7. use shared_child::SharedChild;
  8. use std::{
  9. fs,
  10. io::{BufReader, ErrorKind, Write},
  11. path::PathBuf,
  12. process::{Command, ExitStatus, Stdio},
  13. sync::{
  14. atomic::{AtomicBool, Ordering},
  15. Arc, Mutex,
  16. },
  17. };
  18. pub struct DevChild {
  19. manually_killed_app: Arc<AtomicBool>,
  20. build_child: Option<Arc<SharedChild>>,
  21. app_child: Arc<Mutex<Option<Arc<SharedChild>>>>,
  22. }
  23. impl DevProcess for DevChild {
  24. fn kill(&self) -> std::io::Result<()> {
  25. if let Some(child) = &*self.app_child.lock().unwrap() {
  26. child.kill()?;
  27. } else if let Some(child) = &self.build_child {
  28. child.kill()?;
  29. }
  30. self.manually_killed_app.store(true, Ordering::Relaxed);
  31. Ok(())
  32. }
  33. fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
  34. if let Some(child) = &*self.app_child.lock().unwrap() {
  35. child.try_wait()
  36. } else if let Some(child) = &self.build_child {
  37. child.try_wait()
  38. } else {
  39. unreachable!()
  40. }
  41. }
  42. fn wait(&self) -> std::io::Result<ExitStatus> {
  43. if let Some(child) = &*self.app_child.lock().unwrap() {
  44. child.wait()
  45. } else if let Some(child) = &self.build_child {
  46. child.wait()
  47. } else {
  48. unreachable!()
  49. }
  50. }
  51. fn manually_killed_process(&self) -> bool {
  52. self.manually_killed_app.load(Ordering::Relaxed)
  53. }
  54. }
  55. pub fn run_dev<F: Fn(Option<i32>, ExitReason) + Send + Sync + 'static>(
  56. options: Options,
  57. run_args: Vec<String>,
  58. available_targets: &mut Option<Vec<RustupTarget>>,
  59. config_features: Vec<String>,
  60. app_settings: &RustAppSettings,
  61. main_binary_name: Option<String>,
  62. on_exit: F,
  63. ) -> crate::Result<impl DevProcess> {
  64. let bin_path = app_settings.app_binary_path(&options)?;
  65. let manually_killed_app = Arc::new(AtomicBool::default());
  66. let manually_killed_app_ = manually_killed_app.clone();
  67. let app_child = Arc::new(Mutex::new(None));
  68. let app_child_ = app_child.clone();
  69. let build_child = build_dev_app(
  70. options,
  71. available_targets,
  72. config_features,
  73. move |status, reason| {
  74. if status == Some(0) {
  75. let main_binary_name = main_binary_name.as_deref();
  76. let bin_path = rename_app(bin_path, main_binary_name).expect("failed to rename app");
  77. let mut app = Command::new(bin_path);
  78. app.stdout(os_pipe::dup_stdout().unwrap());
  79. app.stderr(os_pipe::dup_stderr().unwrap());
  80. app.args(run_args);
  81. let app_child = Arc::new(SharedChild::spawn(&mut app).unwrap());
  82. crate::dev::wait_dev_process(
  83. DevChild {
  84. manually_killed_app: manually_killed_app_,
  85. build_child: None,
  86. app_child: Arc::new(Mutex::new(Some(app_child.clone()))),
  87. },
  88. on_exit,
  89. );
  90. app_child_.lock().unwrap().replace(app_child);
  91. } else {
  92. on_exit(
  93. status,
  94. if manually_killed_app_.load(Ordering::Relaxed) {
  95. ExitReason::TriggeredKill
  96. } else {
  97. reason
  98. },
  99. );
  100. }
  101. },
  102. )?;
  103. Ok(DevChild {
  104. manually_killed_app,
  105. build_child: Some(build_child),
  106. app_child,
  107. })
  108. }
  109. pub fn build(
  110. options: Options,
  111. app_settings: &RustAppSettings,
  112. available_targets: &mut Option<Vec<RustupTarget>>,
  113. config_features: Vec<String>,
  114. main_binary_name: Option<&str>,
  115. ) -> crate::Result<PathBuf> {
  116. let out_dir = app_settings.out_dir(&options)?;
  117. let bin_path = app_settings.app_binary_path(&options)?;
  118. if !std::env::var("STATIC_VCRUNTIME").map_or(false, |v| v == "false") {
  119. std::env::set_var("STATIC_VCRUNTIME", "true");
  120. }
  121. if options.target == Some("universal-apple-darwin".into()) {
  122. std::fs::create_dir_all(&out_dir).with_context(|| "failed to create project out directory")?;
  123. let bin_name = bin_path.file_stem().unwrap();
  124. let mut lipo_cmd = Command::new("lipo");
  125. lipo_cmd
  126. .arg("-create")
  127. .arg("-output")
  128. .arg(out_dir.join(bin_name));
  129. for triple in ["aarch64-apple-darwin", "x86_64-apple-darwin"] {
  130. let mut options = options.clone();
  131. options.target.replace(triple.into());
  132. let triple_out_dir = app_settings
  133. .out_dir(&options)
  134. .with_context(|| format!("failed to get {triple} out dir"))?;
  135. build_production_app(options, available_targets, config_features.clone())
  136. .with_context(|| format!("failed to build {triple} binary"))?;
  137. lipo_cmd.arg(triple_out_dir.join(bin_name));
  138. }
  139. let lipo_status = lipo_cmd.output_ok()?.status;
  140. if !lipo_status.success() {
  141. return Err(anyhow::anyhow!(format!(
  142. "Result of `lipo` command was unsuccessful: {lipo_status}. (Is `lipo` installed?)"
  143. )));
  144. }
  145. } else {
  146. build_production_app(options, available_targets, config_features)
  147. .with_context(|| "failed to build app")?;
  148. }
  149. rename_app(bin_path, main_binary_name)
  150. }
  151. fn build_dev_app<F: FnOnce(Option<i32>, ExitReason) + Send + 'static>(
  152. options: Options,
  153. available_targets: &mut Option<Vec<RustupTarget>>,
  154. config_features: Vec<String>,
  155. on_exit: F,
  156. ) -> crate::Result<Arc<SharedChild>> {
  157. let mut build_cmd = build_command(options, available_targets, config_features)?;
  158. let runner = build_cmd.get_program().to_string_lossy().into_owned();
  159. build_cmd
  160. .env(
  161. "CARGO_TERM_PROGRESS_WIDTH",
  162. terminal::stderr_width()
  163. .map(|width| {
  164. if cfg!(windows) {
  165. std::cmp::min(60, width)
  166. } else {
  167. width
  168. }
  169. })
  170. .unwrap_or(if cfg!(windows) { 60 } else { 80 })
  171. .to_string(),
  172. )
  173. .env("CARGO_TERM_PROGRESS_WHEN", "always");
  174. build_cmd.arg("--color");
  175. build_cmd.arg("always");
  176. build_cmd.stdout(os_pipe::dup_stdout()?);
  177. build_cmd.stderr(Stdio::piped());
  178. let build_child = match SharedChild::spawn(&mut build_cmd) {
  179. Ok(c) => Ok(c),
  180. Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
  181. "`{}` command not found.{}",
  182. runner,
  183. if runner == "cargo" {
  184. " Please follow the Tauri setup guide: https://v2.tauri.app/start/prerequisites/"
  185. } else {
  186. ""
  187. }
  188. )),
  189. Err(e) => Err(e.into()),
  190. }?;
  191. let build_child = Arc::new(build_child);
  192. let build_child_stderr = build_child.take_stderr().unwrap();
  193. let mut stderr = BufReader::new(build_child_stderr);
  194. let stderr_lines = Arc::new(Mutex::new(Vec::new()));
  195. let stderr_lines_ = stderr_lines.clone();
  196. std::thread::spawn(move || {
  197. let mut buf = Vec::new();
  198. let mut lines = stderr_lines_.lock().unwrap();
  199. let mut io_stderr = std::io::stderr();
  200. loop {
  201. buf.clear();
  202. if let Ok(0) = tauri_utils::io::read_line(&mut stderr, &mut buf) {
  203. break;
  204. }
  205. let _ = io_stderr.write_all(&buf);
  206. lines.push(String::from_utf8_lossy(&buf).into_owned());
  207. }
  208. });
  209. let build_child_ = build_child.clone();
  210. std::thread::spawn(move || {
  211. let status = build_child_.wait().expect("failed to build app");
  212. if status.success() {
  213. on_exit(status.code(), ExitReason::NormalExit);
  214. } else {
  215. let is_cargo_compile_error = stderr_lines
  216. .lock()
  217. .unwrap()
  218. .last()
  219. .map(|l| l.contains("could not compile"))
  220. .unwrap_or_default();
  221. stderr_lines.lock().unwrap().clear();
  222. on_exit(
  223. status.code(),
  224. if status.code() == Some(101) && is_cargo_compile_error {
  225. ExitReason::CompilationFailed
  226. } else {
  227. ExitReason::NormalExit
  228. },
  229. );
  230. }
  231. });
  232. Ok(build_child)
  233. }
  234. fn build_production_app(
  235. options: Options,
  236. available_targets: &mut Option<Vec<RustupTarget>>,
  237. config_features: Vec<String>,
  238. ) -> crate::Result<()> {
  239. let mut build_cmd = build_command(options, available_targets, config_features)?;
  240. let runner = build_cmd.get_program().to_string_lossy().into_owned();
  241. match build_cmd.piped() {
  242. Ok(status) if status.success() => Ok(()),
  243. Ok(_) => Err(anyhow::anyhow!("failed to build app")),
  244. Err(e) if e.kind() == ErrorKind::NotFound => Err(anyhow::anyhow!(
  245. "`{}` command not found.{}",
  246. runner,
  247. if runner == "cargo" {
  248. " Please follow the Tauri setup guide: https://v2.tauri.app/start/prerequisites/"
  249. } else {
  250. ""
  251. }
  252. )),
  253. Err(e) => Err(e.into()),
  254. }
  255. }
  256. fn build_command(
  257. options: Options,
  258. available_targets: &mut Option<Vec<RustupTarget>>,
  259. config_features: Vec<String>,
  260. ) -> crate::Result<Command> {
  261. let runner = options.runner.unwrap_or_else(|| "cargo".into());
  262. if let Some(target) = &options.target {
  263. if available_targets.is_none() {
  264. *available_targets = fetch_available_targets();
  265. }
  266. validate_target(available_targets, target)?;
  267. }
  268. let mut args = Vec::new();
  269. if !options.args.is_empty() {
  270. args.extend(options.args);
  271. }
  272. let mut features = config_features;
  273. if let Some(f) = options.features {
  274. features.extend(f);
  275. }
  276. if !features.is_empty() {
  277. args.push("--features".into());
  278. args.push(features.join(","));
  279. }
  280. if !options.debug && !args.contains(&"--profile".to_string()) {
  281. args.push("--release".into());
  282. }
  283. if let Some(target) = options.target {
  284. args.push("--target".into());
  285. args.push(target);
  286. }
  287. let mut build_cmd = Command::new(runner);
  288. build_cmd.arg("build");
  289. build_cmd.args(args);
  290. Ok(build_cmd)
  291. }
  292. fn fetch_available_targets() -> Option<Vec<RustupTarget>> {
  293. if let Ok(output) = Command::new("rustup").args(["target", "list"]).output() {
  294. let stdout = String::from_utf8_lossy(&output.stdout).into_owned();
  295. Some(
  296. stdout
  297. .split('\n')
  298. .map(|t| {
  299. let mut s = t.split(' ');
  300. let name = s.next().unwrap().to_string();
  301. let installed = s.next().map(|v| v == "(installed)").unwrap_or_default();
  302. RustupTarget { name, installed }
  303. })
  304. .filter(|t| !t.name.is_empty())
  305. .collect(),
  306. )
  307. } else {
  308. None
  309. }
  310. }
  311. fn validate_target(
  312. available_targets: &Option<Vec<RustupTarget>>,
  313. target: &str,
  314. ) -> crate::Result<()> {
  315. if let Some(available_targets) = available_targets {
  316. if let Some(target) = available_targets.iter().find(|t| t.name == target) {
  317. if !target.installed {
  318. anyhow::bail!(
  319. "Target {target} is not installed (installed targets: {installed}). Please run `rustup target add {target}`.",
  320. target = target.name,
  321. installed = available_targets.iter().filter(|t| t.installed).map(|t| t.name.as_str()).collect::<Vec<&str>>().join(", ")
  322. );
  323. }
  324. }
  325. if !available_targets.iter().any(|t| t.name == target) {
  326. anyhow::bail!("Target {target} does not exist. Please run `rustup target list` to see the available targets.", target = target);
  327. }
  328. }
  329. Ok(())
  330. }
  331. fn rename_app(bin_path: PathBuf, main_binary_name: Option<&str>) -> crate::Result<PathBuf> {
  332. if let Some(main_binary_name) = main_binary_name {
  333. let new_path = bin_path
  334. .with_file_name(main_binary_name)
  335. .with_extension(bin_path.extension().unwrap_or_default());
  336. fs::rename(&bin_path, &new_path).with_context(|| {
  337. format!(
  338. "failed to rename `{}` to `{}`",
  339. tauri_utils::display_path(bin_path),
  340. tauri_utils::display_path(&new_path),
  341. )
  342. })?;
  343. Ok(new_path)
  344. } else {
  345. Ok(bin_path)
  346. }
  347. }
  348. // taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L514
  349. #[cfg(unix)]
  350. mod terminal {
  351. use std::mem;
  352. pub fn stderr_width() -> Option<usize> {
  353. unsafe {
  354. let mut winsize: libc::winsize = mem::zeroed();
  355. // The .into() here is needed for FreeBSD which defines TIOCGWINSZ
  356. // as c_uint but ioctl wants c_ulong.
  357. #[allow(clippy::useless_conversion)]
  358. if libc::ioctl(libc::STDERR_FILENO, libc::TIOCGWINSZ.into(), &mut winsize) < 0 {
  359. return None;
  360. }
  361. if winsize.ws_col > 0 {
  362. Some(winsize.ws_col as usize)
  363. } else {
  364. None
  365. }
  366. }
  367. }
  368. }
  369. // taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L543
  370. #[cfg(windows)]
  371. mod terminal {
  372. use std::{cmp, mem, ptr};
  373. use windows_sys::{
  374. core::PCSTR,
  375. Win32::{
  376. Foundation::{CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE},
  377. Storage::FileSystem::{CreateFileA, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING},
  378. System::Console::{
  379. GetConsoleScreenBufferInfo, GetStdHandle, CONSOLE_SCREEN_BUFFER_INFO, STD_ERROR_HANDLE,
  380. },
  381. },
  382. };
  383. pub fn stderr_width() -> Option<usize> {
  384. unsafe {
  385. let stdout = GetStdHandle(STD_ERROR_HANDLE);
  386. let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
  387. if GetConsoleScreenBufferInfo(stdout, &mut csbi) != 0 {
  388. return Some((csbi.srWindow.Right - csbi.srWindow.Left) as usize);
  389. }
  390. // On mintty/msys/cygwin based terminals, the above fails with
  391. // INVALID_HANDLE_VALUE. Use an alternate method which works
  392. // in that case as well.
  393. let h = CreateFileA(
  394. "CONOUT$\0".as_ptr() as PCSTR,
  395. GENERIC_READ | GENERIC_WRITE,
  396. FILE_SHARE_READ | FILE_SHARE_WRITE,
  397. ptr::null_mut(),
  398. OPEN_EXISTING,
  399. 0,
  400. std::ptr::null_mut(),
  401. );
  402. if h == INVALID_HANDLE_VALUE {
  403. return None;
  404. }
  405. let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = mem::zeroed();
  406. let rc = GetConsoleScreenBufferInfo(h, &mut csbi);
  407. CloseHandle(h);
  408. if rc != 0 {
  409. let width = (csbi.srWindow.Right - csbi.srWindow.Left) as usize;
  410. // Unfortunately cygwin/mintty does not set the size of the
  411. // backing console to match the actual window size. This
  412. // always reports a size of 80 or 120 (not sure what
  413. // determines that). Use a conservative max of 60 which should
  414. // work in most circumstances. ConEmu does some magic to
  415. // resize the console correctly, but there's no reasonable way
  416. // to detect which kind of terminal we are running in, or if
  417. // GetConsoleScreenBufferInfo returns accurate information.
  418. return Some(cmp::min(60, width));
  419. }
  420. None
  421. }
  422. }
  423. }