From f083ac9524d0861c49ff554f2bd9e79fe93c491f Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Sun, 15 Mar 2026 17:02:32 -0500 Subject: [PATCH] Updated doctor command to work with FreeBSD --- src/commands/doctor.rs | 258 ++++++++++++++++++++++++++--------------- 1 file changed, 165 insertions(+), 93 deletions(-) diff --git a/src/commands/doctor.rs b/src/commands/doctor.rs index 3f3047f..1e0598a 100644 --- a/src/commands/doctor.rs +++ b/src/commands/doctor.rs @@ -43,7 +43,6 @@ pub fn run_diagnostics(fix: bool) -> Result<()> { .bold() ); if fix { - // Check optional tools let optional_missing = !health.cmake_ok || !health.cpp_compiler_ok || !health.git_ok; @@ -76,13 +75,11 @@ pub fn run_diagnostics(fix: bool) -> Result<()> { } pub fn check_system_health() -> SystemHealth { - // arduino-cli let (arduino_cli_ok, arduino_cli_path) = match board::find_arduino_cli() { Some(path) => (true, Some(path.display().to_string())), None => (false, None), }; - // AVR core let avr_core_ok = if let Some(ref path_str) = arduino_cli_path { let path = std::path::Path::new(path_str); board::is_avr_core_installed(path) @@ -90,16 +87,10 @@ pub fn check_system_health() -> SystemHealth { false }; - // avr-size (optional) let avr_size_ok = which::which("avr-size").is_ok(); - - // dialout group (Linux only) let dialout_ok = check_dialout(); - - // cmake (optional -- for host tests) let cmake_ok = which::which("cmake").is_ok(); - // C++ compiler (optional -- for host tests) let cpp_on_path = which::which("g++").is_ok() || which::which("clang++").is_ok() || which::which("cl").is_ok(); @@ -109,10 +100,7 @@ pub fn check_system_health() -> SystemHealth { has_cpp_compiler() }; - // git let git_ok = which::which("git").is_ok(); - - // Serial ports let ports_found = board::list_ports().len(); SystemHealth { @@ -129,7 +117,6 @@ pub fn check_system_health() -> SystemHealth { } } -/// Check for a C++ compiler on any platform. fn has_cpp_compiler() -> bool { if which::which("g++").is_ok() || which::which("clang++").is_ok() { return true; @@ -139,8 +126,6 @@ fn has_cpp_compiler() -> bool { if which::which("cl").is_ok() { return true; } - // cl.exe may be installed via VS Build Tools but not on PATH. - // Check via vswhere.exe (ships with VS installer). if let Ok(output) = std::process::Command::new( r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe", ) @@ -161,7 +146,6 @@ fn print_diagnostics(health: &SystemHealth) { println!("{}", "Required:".bright_yellow().bold()); println!(); - // arduino-cli if health.arduino_cli_ok { println!( " {} arduino-cli {}", @@ -176,7 +160,6 @@ fn print_diagnostics(health: &SystemHealth) { println!(" {} arduino-cli {}", "MISSING".red(), "not found in PATH".red()); } - // AVR core if health.avr_core_ok { println!(" {} arduino:avr core installed", "ok".green()); } else if health.arduino_cli_ok { @@ -197,7 +180,6 @@ fn print_diagnostics(health: &SystemHealth) { println!("{}", "Optional:".bright_yellow().bold()); println!(); - // avr-size if health.avr_size_ok { println!(" {} avr-size (binary size reporting)", "ok".green()); } else if !health.avr_core_ok { @@ -214,32 +196,32 @@ fn print_diagnostics(health: &SystemHealth) { ); } - // dialout #[cfg(unix)] { + let group_name = serial_group_name(); if health.dialout_ok { - println!(" {} user in dialout group", "ok".green()); + println!(" {} user in {} group", "ok".green(), group_name); } else { + let fix_cmd = serial_group_fix_command(); println!( - " {} dialout group {}", + " {} {} group {}", "WARN".yellow(), - "run: sudo usermod -aG dialout $USER".yellow() + group_name, + format!("run: {}", fix_cmd).yellow() ); } } - // cmake if health.cmake_ok { println!(" {} cmake (for host-side tests)", "ok".green()); } else { println!( " {} cmake {}", "na".yellow(), - hint_cmake().yellow() + hint_cmake().as_str().yellow() ); } - // C++ compiler if health.cpp_on_path { println!(" {} C++ compiler", "ok".green()); } else if health.cpp_compiler_ok { @@ -252,18 +234,17 @@ fn print_diagnostics(health: &SystemHealth) { println!( " {} C++ compiler {}", "na".yellow(), - hint_cpp_compiler().yellow() + hint_cpp_compiler().as_str().yellow() ); } - // git if health.git_ok { println!(" {} git", "ok".green()); } else { println!( " {} git {}", "na".yellow(), - hint_git().yellow() + hint_git().as_str().yellow() ); } @@ -278,15 +259,19 @@ fn print_diagnostics(health: &SystemHealth) { health.ports_found ); } else { + let port_hint = if is_freebsd() { + "(plug in a board to detect -- FreeBSD ports appear as /dev/cuaU0)" + } else { + "(plug in a board to detect)" + }; println!( " {} no serial ports {}", "na".yellow(), - "(plug in a board to detect)".yellow() + port_hint.yellow() ); } } -/// Print step-by-step fix instructions when required items are missing. fn print_fix_instructions(health: &SystemHealth) { println!("{}", "How to fix:".bright_cyan().bold()); println!(); @@ -310,11 +295,32 @@ fn print_fix_instructions(health: &SystemHealth) { println!(" Option C -- Direct download:"); println!( " {}", - "https://arduino.github.io/arduino-cli/installation/" - .bright_cyan() + "https://arduino.github.io/arduino-cli/installation/".bright_cyan() ); } else if cfg!(target_os = "macos") { println!(" {}", "brew install arduino-cli".bright_cyan()); + } else if is_freebsd() { + println!(); + println!(" FreeBSD -- download the Linux binary (runs via Linux compat layer):"); + println!( + " {}", + "curl -L https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz -o /tmp/arduino-cli.tar.gz" + .bright_cyan() + ); + println!( + " {}", + "tar xzf /tmp/arduino-cli.tar.gz -C /tmp".bright_cyan() + ); + println!( + " {}", + "sudo mv /tmp/arduino-cli /usr/local/bin/".bright_cyan() + ); + println!(); + println!(" Ensure Linux compat layer is active:"); + println!( + " {}", + "sudo sysrc linux_enable=YES && sudo service linux start".bright_cyan() + ); } else { println!(); println!(" Option A -- Install script:"); @@ -362,46 +368,44 @@ fn print_fix_instructions(health: &SystemHealth) { println!( " {}. {}", step, - "Install the AVR core and verify everything:" - .bright_white() - .bold() + "Install the AVR core and verify everything:".bright_white().bold() ); println!(" {}", "anvil setup".bright_cyan()); println!(); - // step += 1; + } + + #[cfg(unix)] + if !health.dialout_ok { + let fix_cmd = serial_group_fix_command(); + println!( + " {}", + format!( + "Tip: add yourself to the serial group for board upload access: {}", + fix_cmd + ) + .bright_black() + ); + println!(); } if !health.git_ok { println!( " {}", - "Tip: git is optional but recommended for version control." - .bright_black() + "Tip: git is optional but recommended for version control.".bright_black() ); if cfg!(target_os = "windows") { - println!( - " {}", - "winget install Git.Git".bright_black() - ); + println!(" {}", "winget install Git.Git".bright_black()); } else if cfg!(target_os = "macos") { - println!( - " {}", - "xcode-select --install".bright_black() - ); + println!(" {}", "xcode-select --install".bright_black()); + } else if is_freebsd() { + println!(" {}", "sudo pkg install git".bright_black()); } else { - println!( - " {}", - "sudo apt install git".bright_black() - ); + println!(" {}", "sudo apt install git".bright_black()); } println!(); } } -// ========================================================================== -// --fix: automated installation -// ========================================================================== - -/// Prompt the user for yes/no confirmation. fn confirm(prompt: &str) -> bool { print!("{} [Y/n] ", prompt); io::stdout().flush().ok(); @@ -413,7 +417,6 @@ fn confirm(prompt: &str) -> bool { trimmed.is_empty() || trimmed == "y" || trimmed == "yes" } -/// Run a command, streaming output to the terminal. fn run_cmd(program: &str, args: &[&str]) -> bool { println!( " {} {} {}", @@ -428,7 +431,6 @@ fn run_cmd(program: &str, args: &[&str]) -> bool { .unwrap_or(false) } -/// Detect which package manager is available on the system. fn detect_package_manager() -> Option<&'static str> { if cfg!(target_os = "windows") { if which::which("winget").is_ok() { @@ -444,8 +446,13 @@ fn detect_package_manager() -> Option<&'static str> { } else { None } + } else if is_freebsd() { + if which::which("pkg").is_ok() { + Some("pkg") + } else { + None + } } else { - // Linux if which::which("apt").is_ok() { Some("apt") } else if which::which("dnf").is_ok() { @@ -458,11 +465,9 @@ fn detect_package_manager() -> Option<&'static str> { } } -/// Fix required items (arduino-cli, avr core). fn run_fix(health: &SystemHealth) -> Result<()> { let pm = detect_package_manager(); - // -- arduino-cli -- if !health.arduino_cli_ok { println!( "{}", @@ -524,10 +529,39 @@ fn run_fix(health: &SystemHealth) -> Result<()> { println!("{} arduino-cli installed.", "ok".green()); } } - Some("dnf") => { + Some("pkg") => { println!( - " arduino-cli is not in dnf repos. Install manually:" + " {}", + "arduino-cli is not in the FreeBSD ports tree.".bright_yellow() ); + println!( + " {}", + "It runs via the Linux compatibility layer (linux_enable must be YES).".bright_black() + ); + if confirm("Download and install the Linux arduino-cli binary now?") { + if !run_cmd("curl", &[ + "-L", + "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Linux_64bit.tar.gz", + "-o", "/tmp/arduino-cli.tar.gz", + ]) { + println!("{} Download failed. Check your internet connection.", "FAIL".red()); + return Ok(()); + } + run_cmd("tar", &["xzf", "/tmp/arduino-cli.tar.gz", "-C", "/tmp"]); + if !run_cmd("sudo", &["mv", "/tmp/arduino-cli", "/usr/local/bin/arduino-cli"]) { + println!("{} Could not move binary to /usr/local/bin/.", "FAIL".red()); + return Ok(()); + } + println!("{} arduino-cli installed.", "ok".green()); + println!( + " {}", + "If it fails to run, ensure Linux compat is active: sudo sysrc linux_enable=YES && sudo service linux start" + .bright_black() + ); + } + } + Some("dnf") => { + println!(" arduino-cli is not in dnf repos. Install manually:"); println!( " {}", "curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh" @@ -545,19 +579,15 @@ fn run_fix(health: &SystemHealth) -> Result<()> { } } _ => { - println!( - " No supported package manager found. Install manually:" - ); + println!(" No supported package manager found. Install manually:"); println!( " {}", - "https://arduino.github.io/arduino-cli/installation/" - .bright_cyan() + "https://arduino.github.io/arduino-cli/installation/".bright_cyan() ); return Ok(()); } } - // Re-check after install if board::find_arduino_cli().is_none() { println!(); println!( @@ -568,7 +598,6 @@ fn run_fix(health: &SystemHealth) -> Result<()> { } } - // -- AVR core -- if !health.avr_core_ok { if health.arduino_cli_ok || board::find_arduino_cli().is_some() { println!(); @@ -589,13 +618,10 @@ fn run_fix(health: &SystemHealth) -> Result<()> { } } - // Offer optional tools too run_fix_optional(health)?; - Ok(()) } -/// Fix optional items (cmake, C++ compiler, git). fn run_fix_optional(health: &SystemHealth) -> Result<()> { let pm = detect_package_manager(); @@ -666,6 +692,11 @@ fn fix_spec_cmake(pm: Option<&str>) -> FixSpec { program: "sudo", args: &["apt", "install", "-y", "cmake"], }, + Some("pkg") => FixSpec::Auto { + prompt: "Install cmake via pkg?", + program: "sudo", + args: &["pkg", "install", "-y", "cmake"], + }, Some("dnf") => FixSpec::Auto { prompt: "Install cmake via dnf?", program: "sudo", @@ -702,6 +733,9 @@ fn fix_spec_cpp(pm: Option<&str>) -> FixSpec { program: "sudo", args: &["apt", "install", "-y", "g++"], }, + Some("pkg") => FixSpec::Manual { + message: "clang++ is already part of the FreeBSD base system", + }, Some("dnf") => FixSpec::Auto { prompt: "Install g++ via dnf?", program: "sudo", @@ -740,6 +774,11 @@ fn fix_spec_git(pm: Option<&str>) -> FixSpec { program: "sudo", args: &["apt", "install", "-y", "git"], }, + Some("pkg") => FixSpec::Auto { + prompt: "Install git via pkg?", + program: "sudo", + args: &["pkg", "install", "-y", "git"], + }, Some("dnf") => FixSpec::Auto { prompt: "Install git via dnf?", program: "sudo", @@ -756,10 +795,6 @@ fn fix_spec_git(pm: Option<&str>) -> FixSpec { } } -// --------------------------------------------------------------------------- -// Platform-aware install hints (one-liners for the diagnostics table) -// --------------------------------------------------------------------------- - fn hint_avr_size_not_on_path() -> &'static str { if cfg!(target_os = "windows") { "installed but not on PATH (binary size reports will be skipped)" @@ -768,45 +803,82 @@ fn hint_avr_size_not_on_path() -> &'static str { } } -fn hint_cmake() -> &'static str { +fn hint_cmake() -> String { if cfg!(target_os = "windows") { - "install: winget install Kitware.CMake (or choco install cmake)" + "install: winget install Kitware.CMake (or choco install cmake)".into() } else if cfg!(target_os = "macos") { - "install: brew install cmake" + "install: brew install cmake".into() + } else if is_freebsd() { + "install: sudo pkg install cmake".into() } else { - "install: sudo apt install cmake" + match detect_package_manager() { + Some("dnf") => "install: sudo dnf install cmake".into(), + Some("pacman") => "install: sudo pacman -S cmake".into(), + _ => "install: sudo apt install cmake".into(), + } } } -fn hint_cpp_compiler() -> &'static str { +fn hint_cpp_compiler() -> String { if cfg!(target_os = "windows") { - "install: choco install mingw (or open Developer Command Prompt for MSVC)" + "install: choco install mingw (or open Developer Command Prompt for MSVC)".into() } else if cfg!(target_os = "macos") { - "install: xcode-select --install" + "install: xcode-select --install".into() + } else if is_freebsd() { + "clang++ is part of the FreeBSD base system -- check your PATH".into() } else { - "install: sudo apt install g++" + match detect_package_manager() { + Some("dnf") => "install: sudo dnf install gcc-c++".into(), + Some("pacman") => "install: sudo pacman -S gcc".into(), + _ => "install: sudo apt install g++".into(), + } } } -fn hint_git() -> &'static str { +fn hint_git() -> String { if cfg!(target_os = "windows") { - "install: winget install Git.Git (or https://git-scm.com)" + "install: winget install Git.Git (or https://git-scm.com)".into() } else if cfg!(target_os = "macos") { - "install: xcode-select --install (or brew install git)" + "install: xcode-select --install (or brew install git)".into() + } else if is_freebsd() { + "install: sudo pkg install git".into() } else { - "install: sudo apt install git" + match detect_package_manager() { + Some("dnf") => "install: sudo dnf install git".into(), + Some("pacman") => "install: sudo pacman -S git".into(), + _ => "install: sudo apt install git".into(), + } + } +} + +fn is_freebsd() -> bool { + cfg!(target_os = "freebsd") +} + +fn serial_group_name() -> &'static str { + if is_freebsd() { + "dialer" + } else { + "dialout" + } +} + +fn serial_group_fix_command() -> String { + if is_freebsd() { + "sudo pw groupmod dialer -m $USER".to_string() + } else { + "sudo usermod -aG dialout $USER".to_string() } } fn check_dialout() -> bool { #[cfg(unix)] { - let output = std::process::Command::new("groups") - .output(); - match output { + let group = serial_group_name(); + match std::process::Command::new("groups").output() { Ok(out) => { let groups = String::from_utf8_lossy(&out.stdout); - groups.contains("dialout") + groups.contains(group) } Err(_) => false, } @@ -814,6 +886,6 @@ fn check_dialout() -> bool { #[cfg(not(unix))] { - true // Not applicable on Windows + true } } \ No newline at end of file