Cross-compile build works on Windows
Some checks failed
CI / Test (Linux) (push) Has been cancelled
CI / Test (Windows MSVC) (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled

This commit is contained in:
Eric Ratliff
2026-03-17 15:21:13 -05:00
parent aa0d0a33f5
commit 6fe81769a3
4 changed files with 190 additions and 99 deletions

4
Cargo.lock generated
View File

@@ -63,7 +63,7 @@ dependencies = [
[[package]] [[package]]
name = "anvil" name = "anvil"
version = "1.0.0" version = "1.0.1-alpha1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"assert_cmd", "assert_cmd",
@@ -1074,7 +1074,7 @@ checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]] [[package]]
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.1-alpha1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",

View File

@@ -4,7 +4,7 @@ default-members = ["."]
[package] [package]
name = "anvil" name = "anvil"
version = "1.0.0" version = "1.0.1-alpha1"
edition = "2021" edition = "2021"
authors = ["Eric Ratliff <eric@nxlearn.net>"] authors = ["Eric Ratliff <eric@nxlearn.net>"]
description = "Arduino project generator and build tool - forges clean embedded projects" description = "Arduino project generator and build tool - forges clean embedded projects"

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "xtask" name = "xtask"
version = "0.1.0" version = "0.1.1-alpha1"
edition = "2021" edition = "2021"
publish = false publish = false

View File

@@ -63,7 +63,13 @@ struct Target {
archive_ext: &'static str, 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 { Target {
name: "Linux x86_64", name: "Linux x86_64",
triple: "x86_64-unknown-linux-gnu", 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 // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -179,16 +197,12 @@ fn read_version(workspace: &Path) -> Result<String> {
/// Detect host OS /// Detect host OS
fn host_os() -> &'static str { fn host_os() -> &'static str {
if cfg!(target_os = "freebsd") { match std::env::consts::OS {
"freebsd" "freebsd" => "freebsd",
} else if cfg!(target_os = "linux") { "linux" => "linux",
"linux" "macos" => "macos",
} else if cfg!(target_os = "macos") { "windows" => "windows",
"macos" _ => "unknown",
} else if cfg!(target_os = "windows") {
"windows"
} else {
"unknown"
} }
} }
@@ -242,13 +256,21 @@ fn package_binary(
} }
"zip" => { "zip" => {
let archive = artifacts.join(format!("{}.zip", archive_name)); let archive = artifacts.join(format!("{}.zip", archive_name));
// -j = junk paths (store just the filename, not the full path) 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( run(
"zip", "zip",
&["-q", "-j", archive.to_str().unwrap(), binary_path.to_str().unwrap()], &["-q", "-j", archive.to_str().unwrap(), binary_path.to_str().unwrap()],
None, None,
)?; )?;
} }
}
_ => bail!("Unknown archive format: {}", archive_ext), _ => bail!("Unknown archive format: {}", archive_ext),
} }
@@ -342,18 +364,31 @@ fn run_check(_workspace: &Path) -> Result<bool> {
ok(&format!("zig {}", ver)); ok(&format!("zig {}", ver));
} else { } else {
warn("zig -- not found"); 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; missing += 1;
} }
// zip (for Windows archive) // zip -- not needed on Windows (PowerShell Compress-Archive is built in)
if !cfg!(windows) {
if cmd_exists("zip") { if cmd_exists("zip") {
ok("zip"); ok("zip");
} else { } else {
warn("zip -- not found"); warn("zip -- not found");
println!(" {}", "sudo pkg install zip (FreeBSD) or apt install zip (Linux)"); println!(" {}", match host_os() {
"freebsd" => "sudo pkg install zip",
"linux" => "sudo apt install zip",
_ => "install zip",
});
missing += 1; missing += 1;
} }
} else {
ok("zip (PowerShell Compress-Archive -- built in)");
}
// cargo-zigbuild // cargo-zigbuild
if cmd_exists("cargo-zigbuild") { if cmd_exists("cargo-zigbuild") {
@@ -370,7 +405,7 @@ fn run_check(_workspace: &Path) -> Result<bool> {
if cmd_exists("rustup") { if cmd_exists("rustup") {
let targets = run_captured("rustup", &["target", "list", "--installed"]) let targets = run_captured("rustup", &["target", "list", "--installed"])
.unwrap_or_default(); .unwrap_or_default();
for t in TARGETS { for t in cross_targets_for_host(host_os()) {
if targets.contains(t.triple) { if targets.contains(t.triple) {
ok(&format!("rustup target: {}", t.triple)); ok(&format!("rustup target: {}", t.triple));
} else { } else {
@@ -387,13 +422,13 @@ fn run_check(_workspace: &Path) -> Result<bool> {
// Host platform note // Host platform note
let host = host_os(); let host = host_os();
if host == "linux" { if host == "windows" {
println!(); println!();
warn("Running on Linux -- FreeBSD binary cannot be produced."); ok("Windows host -- cargo-zigbuild produces Linux, FreeBSD, and Windows binaries");
println!(" Boot into FreeBSD and run 'cargo xtask' for the FreeBSD binary."); } else if host == "linux" {
} else if host != "freebsd" {
println!(); 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!(); println!();
@@ -410,6 +445,38 @@ fn run_check(_workspace: &Path) -> Result<bool> {
// Fix // 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<String> {
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<String> {
bail!("refresh_windows_path called on non-Windows platform")
}
fn run_fix() -> Result<()> { fn run_fix() -> Result<()> {
header("Installing dependencies"); header("Installing dependencies");
@@ -432,12 +499,36 @@ fn run_fix() -> Result<()> {
bail!("Could not install zig automatically. Install manually: https://ziglang.org/download/"); 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), _ => bail!("Cannot auto-install zig on {}. Install manually: https://ziglang.org/download/", host),
} }
ok("zig installed"); ok("zig installed");
} }
// zip (for Windows archive packaging) // zip (not needed on Windows -- PowerShell Compress-Archive is built in)
if host != "windows" {
if cmd_exists("zip") { if cmd_exists("zip") {
ok("zip already installed"); ok("zip already installed");
} else { } else {
@@ -457,6 +548,9 @@ fn run_fix() -> Result<()> {
} }
ok("zip installed"); ok("zip installed");
} }
} else {
ok("zip not needed on Windows (using PowerShell Compress-Archive)");
}
// cargo-zigbuild // cargo-zigbuild
if cmd_exists("cargo-zigbuild") { if cmd_exists("cargo-zigbuild") {
@@ -471,7 +565,7 @@ fn run_fix() -> Result<()> {
if cmd_exists("rustup") { if cmd_exists("rustup") {
let installed = run_captured("rustup", &["target", "list", "--installed"]) let installed = run_captured("rustup", &["target", "list", "--installed"])
.unwrap_or_default(); .unwrap_or_default();
for t in TARGETS { for t in cross_targets_for_host(host) {
if installed.contains(t.triple) { if installed.contains(t.triple) {
ok(&format!("rustup target {} already installed", t.triple)); ok(&format!("rustup target {} already installed", t.triple));
} else { } else {
@@ -496,7 +590,7 @@ fn run_fix() -> Result<()> {
fn run_clean(workspace: &Path) -> Result<()> { fn run_clean(workspace: &Path) -> Result<()> {
header("Cleaning cross-compile artifacts"); header("Cleaning cross-compile artifacts");
for t in TARGETS { for t in CROSS_TARGETS {
let target_dir = workspace.join("target").join(t.triple); let target_dir = workspace.join("target").join(t.triple);
if target_dir.exists() { if target_dir.exists() {
info(&format!("Removing target/{}/...", t.triple)); info(&format!("Removing target/{}/...", t.triple));
@@ -538,56 +632,53 @@ fn run_build(workspace: &Path, version: &str) -> Result<()> {
} }
fs::create_dir_all(&artifacts)?; fs::create_dir_all(&artifacts)?;
// -- FreeBSD native ---------------------------------------------------- // -- Native binary (current host) -------------------------------------
if host == "freebsd" { let (native_triple, native_name, native_ext) = match host {
header("FreeBSD x86_64"); "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..."); info("Building...");
run("cargo", &["build", "--release"], Some(workspace))?; run("cargo", &["build", "--release"], Some(workspace))?;
let archive_name = format!("anvil-{}-freebsd-x86_64", version); 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)?; 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 ----------------------------------- // -- Cross-compile targets via cargo-zigbuild --------------------------
header("Linux x86_64 (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..."); info("Building...");
run( run(
"cargo", "cargo",
&["zigbuild", "--release", "--target", "x86_64-unknown-linux-gnu"], &["zigbuild", "--release", "--target", t.triple],
Some(workspace), Some(workspace),
)?; )?;
let archive_name = format!("anvil-{}-linux-x86_64", version); let suffix = t.triple
package_binary( .replace("x86_64-unknown-", "")
workspace, .replace("x86_64-pc-", "")
"x86_64-unknown-linux-gnu", .replace("-gnu", "")
"anvil", .replace("freebsd", "freebsd-x86_64")
&archive_name, .replace("linux", "linux-x86_64")
"tar.gz", .replace("windows", "windows-x86_64");
)?; let archive_name = format!("anvil-{}-{}", version, suffix);
ok(&format!("{}.tar.gz", archive_name)); package_binary(workspace, t.triple, t.binary_name, &archive_name, t.archive_ext)?;
ok(&format!("{}.{}", archive_name, t.archive_ext));
// -- 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));
// -- Checksums --------------------------------------------------------- // -- Checksums ---------------------------------------------------------
header("Checksums"); header("Checksums");