diff --git a/tooling/cli/src/interface/rust.rs b/tooling/cli/src/interface/rust.rs index ade606e7c..228c5bb5c 100644 --- a/tooling/cli/src/interface/rust.rs +++ b/tooling/cli/src/interface/rust.rs @@ -4,10 +4,10 @@ use std::{ ffi::OsStr, - fs::{rename, File, FileType}, - io::{BufReader, ErrorKind, Read, Write}, + fs::{File, FileType}, + io::{Read, Write}, path::{Path, PathBuf}, - process::{Command, ExitStatus, Stdio}, + process::ExitStatus, str::FromStr, sync::{ atomic::{AtomicBool, Ordering}, @@ -31,14 +31,12 @@ use tauri_bundler::{ }; use super::{AppSettings, ExitReason, Interface}; -use crate::{ - helpers::{ - app_paths::tauri_dir, - config::{reload as reload_config, wix_settings, Config}, - }, - CommandExt, +use crate::helpers::{ + app_paths::tauri_dir, + config::{reload as reload_config, wix_settings, Config}, }; +mod desktop; mod manifest; use manifest::{rewrite_manifest, Manifest}; @@ -108,7 +106,7 @@ impl DevChild { } #[derive(Debug)] -struct Target { +pub struct Target { name: String, installed: bool, } @@ -159,59 +157,17 @@ impl Interface for Rust { } fn build(&mut self, mut options: Options) -> crate::Result<()> { - let bin_path = self.app_settings.app_binary_path(&options)?; - let out_dir = bin_path.parent().unwrap(); - - let bin_name = bin_path.file_stem().unwrap(); - options .features .get_or_insert(Vec::new()) .push("custom-protocol".into()); - - 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 = self - .app_settings - .out_dir(Some(triple.into()), options.debug) - .with_context(|| format!("failed to get {} out dir", triple))?; - self - .build_production_app(options) - .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 { - self - .build_production_app(options) - .with_context(|| "failed to build app")?; - } - - rename_app(&bin_path, self.product_name.as_deref())?; - + desktop::build( + options, + &self.app_settings, + self.product_name.clone(), + &mut self.available_targets, + self.config_features.clone(), + )?; Ok(()) } @@ -268,50 +224,11 @@ fn lookup(dir: &Path, mut f: F) { } 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, mut options: Options, on_exit: F, ) -> crate::Result { - let bin_path = self.app_settings.app_binary_path(&options)?; - let product_name = self.product_name.clone(); - if !options.args.contains(&"--no-default-features".into()) { let manifest_features = self.app_settings.manifest.features(); let enable_features: Vec = manifest_features @@ -350,51 +267,15 @@ impl Rust { } options.args = args; - 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 = self.build_dev_app(options, 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()); - let app_child_t = app_child.clone(); - std::thread::spawn(move || { - let status = app_child_t.wait().expect("failed to wait on app"); - on_exit( - status, - if manually_killed_app_.load(Ordering::Relaxed) { - ExitReason::TriggeredKill - } else { - ExitReason::NormalExit - }, - ); - }); - - 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, - app_child, - }) + desktop::run_dev( + options, + run_args, + &mut self.available_targets, + self.config_features.clone(), + &self.app_settings, + self.product_name.clone(), + on_exit, + ) } fn run_dev_watcher( @@ -480,158 +361,6 @@ impl Rust { } } } - - fn build_production_app(&mut self, options: Options) -> crate::Result<()> { - let mut build_cmd = self.build_command(options)?; - 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_dev_app( - &mut self, - options: Options, - on_exit: F, - ) -> crate::Result> { - let mut build_cmd = self.build_command(options)?; - 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_command(&mut self, options: Options) -> crate::Result { - let runner = options.runner.unwrap_or_else(|| "cargo".into()); - - if let Some(target) = &options.target { - if self.available_targets.is_none() { - self.fetch_available_targets(); - } - self.validate_target(target)?; - } - - let mut args = Vec::new(); - if !options.args.is_empty() { - args.extend(options.args); - } - - let mut features = self.config_features.clone(); - 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) - } } /// The `workspace` section of the app configuration (read from Cargo.toml). @@ -1153,106 +882,3 @@ fn tauri_config_to_bundle_settings( ..Default::default() }) } - -fn rename_app(bin_path: &Path, product_name: Option<&str>) -> crate::Result { - 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()) - } -} - -// taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L514 -#[cfg(unix)] -mod terminal { - use std::mem; - - pub fn stderr_width() -> Option { - unsafe { - let mut winsize: libc::winsize = mem::zeroed(); - // The .into() here is needed for FreeBSD which defines TIOCGWINSZ - // as c_uint but ioctl wants c_ulong. - #[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 - } - } - } -} - -// taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L543 -#[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 { - 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); - } - - // On mintty/msys/cygwin based terminals, the above fails with - // INVALID_HANDLE_VALUE. Use an alternate method which works - // in that case as well. - 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; - // Unfortunately cygwin/mintty does not set the size of the - // backing console to match the actual window size. This - // always reports a size of 80 or 120 (not sure what - // determines that). Use a conservative max of 60 which should - // work in most circumstances. ConEmu does some magic to - // resize the console correctly, but there's no reasonable way - // to detect which kind of terminal we are running in, or if - // GetConsoleScreenBufferInfo returns accurate information. - return Some(cmp::min(60, width)); - } - - None - } - } -} diff --git a/tooling/cli/src/interface/rust/desktop.rs b/tooling/cli/src/interface/rust/desktop.rs new file mode 100644 index 000000000..b93a3d826 --- /dev/null +++ b/tooling/cli/src/interface/rust/desktop.rs @@ -0,0 +1,436 @@ +use super::{AppSettings, DevChild, ExitReason, Options, RustAppSettings, Target}; +use crate::CommandExt; + +use anyhow::Context; +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 fn run_dev( + options: Options, + run_args: Vec, + available_targets: &mut Option>, + config_features: Vec, + app_settings: &RustAppSettings, + product_name: Option, + on_exit: F, +) -> crate::Result { + 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()); + let app_child_t = app_child.clone(); + std::thread::spawn(move || { + let status = app_child_t.wait().expect("failed to wait on app"); + on_exit( + status, + if manually_killed_app_.load(Ordering::Relaxed) { + ExitReason::TriggeredKill + } else { + ExitReason::NormalExit + }, + ); + }); + + 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, + app_child, + }) +} + +pub fn build( + options: Options, + app_settings: &RustAppSettings, + product_name: Option, + available_targets: &mut Option>, + config_features: Vec, +) -> 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( + options: Options, + available_targets: &mut Option>, + config_features: Vec, + on_exit: F, +) -> crate::Result> { + 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>, + config_features: Vec, +) -> 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>, + config_features: Vec, +) -> crate::Result { + 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> { + 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>, 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::>().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 { + 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()) + } +} + +// taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L514 +#[cfg(unix)] +mod terminal { + use std::mem; + + pub fn stderr_width() -> Option { + unsafe { + let mut winsize: libc::winsize = mem::zeroed(); + // The .into() here is needed for FreeBSD which defines TIOCGWINSZ + // as c_uint but ioctl wants c_ulong. + #[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 + } + } + } +} + +// taken from https://github.com/rust-lang/cargo/blob/78b10d4e611ab0721fc3aeaf0edd5dd8f4fdc372/src/cargo/core/shell.rs#L543 +#[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 { + 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); + } + + // On mintty/msys/cygwin based terminals, the above fails with + // INVALID_HANDLE_VALUE. Use an alternate method which works + // in that case as well. + 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; + // Unfortunately cygwin/mintty does not set the size of the + // backing console to match the actual window size. This + // always reports a size of 80 or 120 (not sure what + // determines that). Use a conservative max of 60 which should + // work in most circumstances. ConEmu does some magic to + // resize the console correctly, but there's no reasonable way + // to detect which kind of terminal we are running in, or if + // GetConsoleScreenBufferInfo returns accurate information. + return Some(cmp::min(60, width)); + } + + None + } + } +}