2 Commits

Author SHA1 Message Date
Eric Ratliff
6fe81769a3 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
2026-03-17 15:21:13 -05:00
Eric Ratliff
aa0d0a33f5 Added a potential roadmap file
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
2026-03-17 08:37:46 -05:00
5 changed files with 204 additions and 99 deletions

4
Cargo.lock generated
View File

@@ -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",

View File

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

14
docs/ROADMAP.md Normal file
View File

@@ -0,0 +1,14 @@
v1.1.0 — Template richness + uninstall
- Template metadata (template.toml with descriptions, file counts, test counts)
- anvil uninstall (remove arduino-cli, AVR core, etc.)
- More templates: servo, i2c, pid-control
v1.2.0 — Community library ecosystem
- Remote library registry (packages.nxgit.dev)
- anvil add nexus/sensors/ultrasonic
- anvil search, anvil update
- Versioned library dependencies in .anvil.toml
v1.3.0 — Developer experience
- Windows MSI installer (cargo-wix)
- GUI (Tauri) for teams uncomfortable with terminals

View File

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

View File

@@ -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<String> {
/// 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<bool> {
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<bool> {
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<bool> {
// 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<bool> {
// 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<()> {
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");