desktop.rs 13 KB

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