From 6fe81769a39c2f432cd6e93588dd4369d53f660b Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Tue, 17 Mar 2026 15:21:13 -0500 Subject: [PATCH] Cross-compile build works on Windows --- Cargo.lock | 4 +- Cargo.toml | 2 +- xtask/Cargo.toml | 2 +- xtask/src/main.rs | 281 ++++++++++++++++++++++++++++++---------------- 4 files changed, 190 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3050673..5586c7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,7 +63,7 @@ dependencies = [ [[package]] name = "anvil" -version = "1.0.0" +version = "1.0.1-alpha1" dependencies = [ "anyhow", "assert_cmd", @@ -1074,7 +1074,7 @@ checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] name = "xtask" -version = "0.1.0" +version = "0.1.1-alpha1" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 9f79ea4..6a880d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ default-members = ["."] [package] name = "anvil" -version = "1.0.0" +version = "1.0.1-alpha1" edition = "2021" authors = ["Eric Ratliff "] description = "Arduino project generator and build tool - forges clean embedded projects" diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml index c93d82b..16c3955 100644 --- a/xtask/Cargo.toml +++ b/xtask/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "xtask" -version = "0.1.0" +version = "0.1.1-alpha1" edition = "2021" publish = false diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 1ef9a88..0fd3f2d 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -63,7 +63,13 @@ struct Target { archive_ext: &'static str, } -const TARGETS: &[Target] = &[ +const CROSS_TARGETS: &[Target] = &[ + Target { + name: "FreeBSD x86_64", + triple: "x86_64-unknown-freebsd", + binary_name: "anvil", + archive_ext: "tar.gz", + }, Target { name: "Linux x86_64", triple: "x86_64-unknown-linux-gnu", @@ -78,6 +84,18 @@ const TARGETS: &[Target] = &[ }, ]; +/// Returns cross-compile targets for the current host, skipping the native target. +fn cross_targets_for_host(host: &str) -> Vec<&'static Target> { + CROSS_TARGETS.iter().filter(|t| { + match host { + "freebsd" => t.triple != "x86_64-unknown-freebsd", + "linux" => t.triple != "x86_64-unknown-linux-gnu", + "windows" => t.triple != "x86_64-pc-windows-gnu", + _ => true, + } + }).collect() +} + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -179,16 +197,12 @@ fn read_version(workspace: &Path) -> Result { /// Detect host OS fn host_os() -> &'static str { - if cfg!(target_os = "freebsd") { - "freebsd" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "macos") { - "macos" - } else if cfg!(target_os = "windows") { - "windows" - } else { - "unknown" + match std::env::consts::OS { + "freebsd" => "freebsd", + "linux" => "linux", + "macos" => "macos", + "windows" => "windows", + _ => "unknown", } } @@ -242,12 +256,20 @@ fn package_binary( } "zip" => { let archive = artifacts.join(format!("{}.zip", archive_name)); - // -j = junk paths (store just the filename, not the full path) - run( - "zip", - &["-q", "-j", archive.to_str().unwrap(), binary_path.to_str().unwrap()], - None, - )?; + if cfg!(windows) { + let ps_cmd = format!( + "Compress-Archive -Path '{}' -DestinationPath '{}' -Force", + binary_path.display(), + archive.display() + ); + run("powershell", &["-NoProfile", "-Command", &ps_cmd], None)?; + } else { + run( + "zip", + &["-q", "-j", archive.to_str().unwrap(), binary_path.to_str().unwrap()], + None, + )?; + } } _ => bail!("Unknown archive format: {}", archive_ext), } @@ -342,17 +364,30 @@ fn run_check(_workspace: &Path) -> Result { ok(&format!("zig {}", ver)); } else { warn("zig -- not found"); - println!(" {}", "sudo pkg install zig (FreeBSD) or apt install zig (Linux)"); + println!(" {}", match host_os() { + "freebsd" => "sudo pkg install zig", + "linux" => "sudo apt install zig (or: sudo dnf install zig)", + "windows" => "winget install zig.zig (or: choco install zig)", + _ => "see https://ziglang.org/download/", + }); missing += 1; } - // zip (for Windows archive) - if cmd_exists("zip") { - ok("zip"); + // zip -- not needed on Windows (PowerShell Compress-Archive is built in) + if !cfg!(windows) { + if cmd_exists("zip") { + ok("zip"); + } else { + warn("zip -- not found"); + println!(" {}", match host_os() { + "freebsd" => "sudo pkg install zip", + "linux" => "sudo apt install zip", + _ => "install zip", + }); + missing += 1; + } } else { - warn("zip -- not found"); - println!(" {}", "sudo pkg install zip (FreeBSD) or apt install zip (Linux)"); - missing += 1; + ok("zip (PowerShell Compress-Archive -- built in)"); } // cargo-zigbuild @@ -370,7 +405,7 @@ fn run_check(_workspace: &Path) -> Result { if cmd_exists("rustup") { let targets = run_captured("rustup", &["target", "list", "--installed"]) .unwrap_or_default(); - for t in TARGETS { + for t in cross_targets_for_host(host_os()) { if targets.contains(t.triple) { ok(&format!("rustup target: {}", t.triple)); } else { @@ -387,13 +422,13 @@ fn run_check(_workspace: &Path) -> Result { // Host platform note let host = host_os(); - if host == "linux" { + if host == "windows" { println!(); - warn("Running on Linux -- FreeBSD binary cannot be produced."); - println!(" Boot into FreeBSD and run 'cargo xtask' for the FreeBSD binary."); - } else if host != "freebsd" { + ok("Windows host -- cargo-zigbuild produces Linux, FreeBSD, and Windows binaries"); + } else if host == "linux" { println!(); - warn(&format!("Unsupported host OS: {}", host)); + ok("Linux host -- cargo-zigbuild produces FreeBSD and Windows binaries"); + println!(" (native Linux binary built with plain cargo)"); } println!(); @@ -410,6 +445,38 @@ fn run_check(_workspace: &Path) -> Result { // Fix // --------------------------------------------------------------------------- +/// On Windows, reads the current Machine + User PATH from the registry and +/// returns a merged PATH string so the current process can find newly +/// installed tools without requiring a new terminal session. +#[cfg(windows)] +fn refresh_windows_path() -> Result { + use std::process::Command; + // Read system PATH + let sys = Command::new("powershell") + .args(&[ + "-NoProfile", "-Command", + "[System.Environment]::GetEnvironmentVariable('PATH','Machine')" + ]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default(); + // Read user PATH + let usr = Command::new("powershell") + .args(&[ + "-NoProfile", "-Command", + "[System.Environment]::GetEnvironmentVariable('PATH','User')" + ]) + .output() + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) + .unwrap_or_default(); + Ok(format!("{};{}", sys, usr)) +} + +#[cfg(not(windows))] +fn refresh_windows_path() -> Result { + bail!("refresh_windows_path called on non-Windows platform") +} + fn run_fix() -> Result<()> { header("Installing dependencies"); @@ -432,30 +499,57 @@ fn run_fix() -> Result<()> { bail!("Could not install zig automatically. Install manually: https://ziglang.org/download/"); } } + "windows" => { + if cmd_exists("winget") { + run("winget", &["install", "--id", "zig.zig", "-e", "--silent"], None)?; + // Refresh PATH in the current process so zig is findable immediately. + // winget updates the registry but the current session won't see it + // without this -- otherwise the user needs to open a new terminal. + if let Ok(new_path) = refresh_windows_path() { + unsafe { + std::env::set_var("PATH", &new_path); + } + } + if !cmd_exists("zig") { + println!(); + warn("zig was installed but is not yet on PATH."); + println!(" Open a new terminal and run: cargo xtask"); + println!(" (Windows PATH updates require a new shell session)"); + } + } else if cmd_exists("choco") { + run("choco", &["install", "zig", "-y"], None)?; + } else { + bail!("Install zig via winget: winget install zig.zig\n Or download from: https://ziglang.org/download/"); + } + } _ => bail!("Cannot auto-install zig on {}. Install manually: https://ziglang.org/download/", host), } ok("zig installed"); } - // zip (for Windows archive packaging) - if cmd_exists("zip") { - ok("zip already installed"); - } else { - info("Installing zip..."); - match host { - "freebsd" => run("sudo", &["pkg", "install", "-y", "zip"], None)?, - "linux" => { - if cmd_exists("apt-get") { - run("sudo", &["apt-get", "install", "-y", "zip"], None)?; - } else if cmd_exists("dnf") { - run("sudo", &["dnf", "install", "-y", "zip"], None)?; - } else { - bail!("Could not install zip automatically. Install it manually."); + // zip (not needed on Windows -- PowerShell Compress-Archive is built in) + if host != "windows" { + if cmd_exists("zip") { + ok("zip already installed"); + } else { + info("Installing zip..."); + match host { + "freebsd" => run("sudo", &["pkg", "install", "-y", "zip"], None)?, + "linux" => { + if cmd_exists("apt-get") { + run("sudo", &["apt-get", "install", "-y", "zip"], None)?; + } else if cmd_exists("dnf") { + run("sudo", &["dnf", "install", "-y", "zip"], None)?; + } else { + bail!("Could not install zip automatically. Install it manually."); + } } + _ => bail!("Cannot auto-install zip on {}. Install it manually.", host), } - _ => bail!("Cannot auto-install zip on {}. Install it manually.", host), + ok("zip installed"); } - ok("zip installed"); + } else { + ok("zip not needed on Windows (using PowerShell Compress-Archive)"); } // cargo-zigbuild @@ -471,7 +565,7 @@ fn run_fix() -> Result<()> { if cmd_exists("rustup") { let installed = run_captured("rustup", &["target", "list", "--installed"]) .unwrap_or_default(); - for t in TARGETS { + for t in cross_targets_for_host(host) { if installed.contains(t.triple) { ok(&format!("rustup target {} already installed", t.triple)); } else { @@ -496,7 +590,7 @@ fn run_fix() -> Result<()> { fn run_clean(workspace: &Path) -> Result<()> { header("Cleaning cross-compile artifacts"); - for t in TARGETS { + for t in CROSS_TARGETS { let target_dir = workspace.join("target").join(t.triple); if target_dir.exists() { info(&format!("Removing target/{}/...", t.triple)); @@ -538,56 +632,53 @@ fn run_build(workspace: &Path, version: &str) -> Result<()> { } fs::create_dir_all(&artifacts)?; - // -- FreeBSD native ---------------------------------------------------- - if host == "freebsd" { - header("FreeBSD x86_64"); - info("Building..."); - run("cargo", &["build", "--release"], Some(workspace))?; - let archive_name = format!("anvil-{}-freebsd-x86_64", version); + // -- Native binary (current host) ------------------------------------- + let (native_triple, native_name, native_ext) = match host { + "freebsd" => ("freebsd-x86_64", "anvil", "tar.gz"), + "linux" => ("linux-x86_64", "anvil", "tar.gz"), + "windows" => ("windows-x86_64", "anvil.exe", "zip"), + _ => ("unknown", "anvil", "tar.gz"), + }; + header(&format!("{} (native)", native_name.to_uppercase().replace("ANVIL", &format!("{} x86_64", host.to_uppercase().replace("FREEBSD", "FreeBSD").replace("LINUX", "Linux").replace("WINDOWS", "Windows"))))); + info("Building..."); + run("cargo", &["build", "--release"], Some(workspace))?; + let archive_name = format!("anvil-{}-{}", version, native_triple); + if native_ext == "zip" { + // Windows native -- package exe directly + let binary_path = workspace.join("target").join("release").join(native_name); + package_binary(workspace, "", native_name, &archive_name, "zip")?; + let _ = binary_path; // used inside package_binary + } else { package_native(workspace, &archive_name)?; - ok(&format!("{}.tar.gz", archive_name)); - } else if host == "linux" { - println!(); - warn("Running on Linux -- FreeBSD binary cannot be produced."); - println!(" Boot into FreeBSD and run 'cargo xtask' for the FreeBSD binary."); - println!(); } + ok(&format!("{}.{}", archive_name, native_ext)); - // -- Linux x86_64 via cargo-zigbuild ----------------------------------- - header("Linux x86_64 (via cargo-zigbuild)"); - info("Building..."); - run( - "cargo", - &["zigbuild", "--release", "--target", "x86_64-unknown-linux-gnu"], - Some(workspace), - )?; - let archive_name = format!("anvil-{}-linux-x86_64", version); - package_binary( - workspace, - "x86_64-unknown-linux-gnu", - "anvil", - &archive_name, - "tar.gz", - )?; - ok(&format!("{}.tar.gz", archive_name)); - - // -- Windows x86_64 via cargo-zigbuild --------------------------------- - header("Windows x86_64 (via cargo-zigbuild)"); - info("Building..."); - run( - "cargo", - &["zigbuild", "--release", "--target", "x86_64-pc-windows-gnu"], - Some(workspace), - )?; - let archive_name = format!("anvil-{}-windows-x86_64", version); - package_binary( - workspace, - "x86_64-pc-windows-gnu", - "anvil.exe", - &archive_name, - "zip", - )?; - ok(&format!("{}.zip", archive_name)); + // -- Cross-compile targets via cargo-zigbuild -------------------------- + for t in cross_targets_for_host(host) { + let label = match t.triple { + "x86_64-unknown-freebsd" => "FreeBSD x86_64", + "x86_64-unknown-linux-gnu" => "Linux x86_64", + "x86_64-pc-windows-gnu" => "Windows x86_64", + other => other, + }; + header(&format!("{} (via cargo-zigbuild)", label)); + info("Building..."); + run( + "cargo", + &["zigbuild", "--release", "--target", t.triple], + Some(workspace), + )?; + let suffix = t.triple + .replace("x86_64-unknown-", "") + .replace("x86_64-pc-", "") + .replace("-gnu", "") + .replace("freebsd", "freebsd-x86_64") + .replace("linux", "linux-x86_64") + .replace("windows", "windows-x86_64"); + let archive_name = format!("anvil-{}-{}", version, suffix); + package_binary(workspace, t.triple, t.binary_name, &archive_name, t.archive_ext)?; + ok(&format!("{}.{}", archive_name, t.archive_ext)); + } // -- Checksums --------------------------------------------------------- header("Checksums");