Compare commits
3 Commits
79f6bb57d4
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7995de547 | ||
|
|
13ab202880 | ||
|
|
bb4b2f4162 |
3
.cargo/config.toml
Normal file
3
.cargo/config.toml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# This alias makes `cargo xtask` work -- it runs the xtask workspace member.
|
||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
||||||
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -1072,6 +1072,16 @@ version = "0.51.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"clap",
|
||||||
|
"colored",
|
||||||
|
"which",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
[workspace]
|
||||||
|
members = [".", "xtask"]
|
||||||
|
default-members = ["."]
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "anvil"
|
name = "anvil"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|||||||
112
README.md
112
README.md
@@ -377,65 +377,18 @@ Anvil is written in Rust and compiles to a single static binary. You need:
|
|||||||
|
|
||||||
- **Rust toolchain** (stable, 2021 edition or later)
|
- **Rust toolchain** (stable, 2021 edition or later)
|
||||||
- **A C linker** (`gcc` or equivalent -- Rust uses it under the hood)
|
- **A C linker** (`gcc` or equivalent -- Rust uses it under the hood)
|
||||||
- **MinGW** (only if cross-compiling a Windows binary from Linux)
|
|
||||||
- **zip** (only for packaging release artifacts)
|
|
||||||
|
|
||||||
### Linux / WSL from scratch
|
### Build
|
||||||
|
|
||||||
A fresh Ubuntu or WSL instance needs three commands:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. System packages (C linker + cross-compile + packaging tools)
|
|
||||||
sudo apt update && sudo apt install build-essential mingw-w64 zip
|
|
||||||
|
|
||||||
# 2. Rust toolchain
|
|
||||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
|
||||||
source ~/.cargo/env
|
|
||||||
|
|
||||||
# 3. Build
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
The release binary lands at `target/release/anvil`. Copy it somewhere in
|
The release binary lands at `target/release/anvil` (or `target\release\anvil.exe`
|
||||||
your PATH.
|
on Windows). Copy it somewhere in your PATH.
|
||||||
|
|
||||||
### Windows (native)
|
|
||||||
|
|
||||||
Install Rust from [rustup.rs](https://rustup.rs), which includes the MSVC
|
|
||||||
toolchain. Then:
|
|
||||||
|
|
||||||
```
|
|
||||||
cargo build --release
|
|
||||||
```
|
|
||||||
|
|
||||||
The binary lands at `target\release\anvil.exe`.
|
|
||||||
|
|
||||||
### Release builds (Linux + Windows from one machine)
|
|
||||||
|
|
||||||
The `build-release.sh` script at the repo root builds optimized, stripped
|
|
||||||
binaries for both platforms and packages them into tarballs and zips. It
|
|
||||||
reads the version from `Cargo.toml` (the single source of truth) and
|
|
||||||
accepts an optional suffix for pre-release builds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build-release.sh # uses version from Cargo.toml (e.g. 1.0.0)
|
|
||||||
./build-release.sh beta1 # appends suffix (e.g. 1.0.0-beta1)
|
|
||||||
./build-release.sh rc1 # appends suffix (e.g. 1.0.0-rc1)
|
|
||||||
```
|
|
||||||
|
|
||||||
This produces a `release-artifacts/` directory with:
|
|
||||||
|
|
||||||
```
|
|
||||||
anvil-1.0.0-linux-x86_64.tar.gz
|
|
||||||
anvil-1.0.0-linux-x86_64.zip
|
|
||||||
anvil-1.0.0-windows-x86_64.zip
|
|
||||||
SHA256SUMS
|
|
||||||
```
|
|
||||||
|
|
||||||
Upload these to a Gitea release. The script requires `build-essential`,
|
|
||||||
`mingw-w64`, and `zip` as described above.
|
|
||||||
|
|
||||||
### Running the test suite
|
### Running the test suite
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
@@ -449,6 +402,7 @@ skip gracefully and everything else still passes.
|
|||||||
#### Full test suite on Linux / WSL
|
#### Full test suite on Linux / WSL
|
||||||
|
|
||||||
The e2e tests need `cmake`, `g++`, and `arduino-cli` with the AVR core:
|
The e2e tests need `cmake`, `g++`, and `arduino-cli` with the AVR core:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install cmake g++
|
sudo apt install cmake g++
|
||||||
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
||||||
@@ -460,6 +414,7 @@ arduino-cli core install arduino:avr
|
|||||||
|
|
||||||
Install [arduino-cli](https://arduino.github.io/arduino-cli/installation/)
|
Install [arduino-cli](https://arduino.github.io/arduino-cli/installation/)
|
||||||
and add it to your PATH, then install the AVR core:
|
and add it to your PATH, then install the AVR core:
|
||||||
|
|
||||||
```
|
```
|
||||||
arduino-cli core install arduino:avr
|
arduino-cli core install arduino:avr
|
||||||
```
|
```
|
||||||
@@ -470,6 +425,61 @@ a Visual Studio Developer Command Prompt (which provides `cl.exe`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Building Release Binaries
|
||||||
|
|
||||||
|
Anvil uses a Rust `xtask` workspace member to build release binaries for all
|
||||||
|
platforms from a single machine. No VMs, no containers, no root required.
|
||||||
|
|
||||||
|
### How it works
|
||||||
|
|
||||||
|
The xtask build system uses [cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild)
|
||||||
|
with the [Zig](https://ziglang.org) compiler as a universal cross-linker.
|
||||||
|
From a single FreeBSD machine you get all three platform binaries:
|
||||||
|
|
||||||
|
```
|
||||||
|
release-artifacts/
|
||||||
|
anvil-X.Y.Z-freebsd-x86_64.tar.gz native FreeBSD build
|
||||||
|
anvil-X.Y.Z-linux-x86_64.tar.gz cross-compiled via zig
|
||||||
|
anvil-X.Y.Z-windows-x86_64.zip cross-compiled via zig
|
||||||
|
SHA256SUMS
|
||||||
|
```
|
||||||
|
|
||||||
|
### First-time setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs zig (via pkg), zip, cargo-zigbuild, and the required rustup
|
||||||
|
cross-compile targets. Only the zig and zip installation steps require sudo.
|
||||||
|
|
||||||
|
### Building a release
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask # build all platforms, version from Cargo.toml
|
||||||
|
cargo xtask --suffix rc1 # pre-release (e.g. 1.0.0-rc1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo xtask --check # verify all dependencies are installed
|
||||||
|
cargo xtask --clean # remove cross-compile artifacts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platform support
|
||||||
|
|
||||||
|
| Host | FreeBSD binary | Linux binary | Windows binary |
|
||||||
|
|---|---|---|---|
|
||||||
|
| FreeBSD | native | cargo-zigbuild | cargo-zigbuild |
|
||||||
|
| Linux | not possible | native | cargo-zigbuild |
|
||||||
|
| Windows | not possible | not tested | native |
|
||||||
|
|
||||||
|
FreeBSD is the recommended build host -- it produces all three binaries in
|
||||||
|
a single run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT -- see [LICENSE](LICENSE).
|
MIT -- see [LICENSE](LICENSE).
|
||||||
101
build-release.sh
101
build-release.sh
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Build release binaries for distribution
|
|
||||||
# This script builds both Linux and Windows binaries (cross-compile)
|
|
||||||
#
|
|
||||||
# Usage:
|
|
||||||
# ./build-release.sh # version from Cargo.toml (e.g. 1.0.0)
|
|
||||||
# ./build-release.sh beta1 # appends suffix (e.g. 1.0.0-beta1)
|
|
||||||
# ./build-release.sh rc1 # appends suffix (e.g. 1.0.0-rc1)
|
|
||||||
#
|
|
||||||
# For Windows-only builds, use build-release.ps1 on Windows
|
|
||||||
# For Linux-only builds, comment out the Windows section below
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Read version from the single source of truth: Cargo.toml
|
|
||||||
BASE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/' | tr -d '\r')
|
|
||||||
if [ -z "$BASE_VERSION" ]; then
|
|
||||||
echo "Error: Could not read version from Cargo.toml"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Optional suffix (beta1, rc1, etc.)
|
|
||||||
if [ -n "$1" ]; then
|
|
||||||
VERSION="${BASE_VERSION}-${1}"
|
|
||||||
else
|
|
||||||
VERSION="$BASE_VERSION"
|
|
||||||
fi
|
|
||||||
|
|
||||||
RELEASE_DIR="release-artifacts"
|
|
||||||
|
|
||||||
echo "Building Anvil v${VERSION} release binaries..."
|
|
||||||
echo " Version base: ${BASE_VERSION} (from Cargo.toml)"
|
|
||||||
if [ -n "$1" ]; then
|
|
||||||
echo " Suffix: ${1}"
|
|
||||||
fi
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Clean previous artifacts
|
|
||||||
rm -rf "$RELEASE_DIR"
|
|
||||||
mkdir -p "$RELEASE_DIR"
|
|
||||||
|
|
||||||
# Build Linux binary (optimized)
|
|
||||||
echo "Building Linux x86_64 binary..."
|
|
||||||
cargo build --release
|
|
||||||
strip target/release/anvil
|
|
||||||
|
|
||||||
# Package Linux binary
|
|
||||||
echo "Packaging Linux binaries..."
|
|
||||||
cd target/release
|
|
||||||
tar -czf "../../$RELEASE_DIR/anvil-${VERSION}-linux-x86_64.tar.gz" anvil
|
|
||||||
zip -q "../../$RELEASE_DIR/anvil-${VERSION}-linux-x86_64.zip" anvil
|
|
||||||
cd ../..
|
|
||||||
|
|
||||||
# Build Windows binary (cross-compile)
|
|
||||||
echo ""
|
|
||||||
echo "Building Windows x86_64 binary..."
|
|
||||||
|
|
||||||
# Check if Windows target is installed
|
|
||||||
if ! rustup target list | grep -q "x86_64-pc-windows-gnu (installed)"; then
|
|
||||||
echo "Installing Windows target..."
|
|
||||||
rustup target add x86_64-pc-windows-gnu
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if MinGW is installed
|
|
||||||
if ! command -v x86_64-w64-mingw32-gcc &> /dev/null; then
|
|
||||||
echo "Warning: MinGW not found. Install with: sudo apt install mingw-w64"
|
|
||||||
echo "Skipping Windows build."
|
|
||||||
else
|
|
||||||
cargo build --release --target x86_64-pc-windows-gnu
|
|
||||||
x86_64-w64-mingw32-strip target/x86_64-pc-windows-gnu/release/anvil.exe
|
|
||||||
|
|
||||||
# Package Windows binary
|
|
||||||
echo "Packaging Windows binary..."
|
|
||||||
cd target/x86_64-pc-windows-gnu/release
|
|
||||||
zip -q "../../../$RELEASE_DIR/anvil-${VERSION}-windows-x86_64.zip" anvil.exe
|
|
||||||
cd ../../..
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Generate checksums
|
|
||||||
echo ""
|
|
||||||
echo "Generating checksums..."
|
|
||||||
cd "$RELEASE_DIR"
|
|
||||||
sha256sum * > SHA256SUMS
|
|
||||||
cd ..
|
|
||||||
|
|
||||||
# Display results
|
|
||||||
echo ""
|
|
||||||
echo "==========================================================="
|
|
||||||
echo " Anvil v${VERSION} release artifacts built successfully!"
|
|
||||||
echo "==========================================================="
|
|
||||||
echo ""
|
|
||||||
echo "Artifacts in $RELEASE_DIR/:"
|
|
||||||
ls -lh "$RELEASE_DIR"
|
|
||||||
echo ""
|
|
||||||
echo "Checksums:"
|
|
||||||
cat "$RELEASE_DIR/SHA256SUMS"
|
|
||||||
echo ""
|
|
||||||
echo "Upload these files to your Gitea release:"
|
|
||||||
echo " 1. Create release: Releases -> New Release -> Tag: v${VERSION}"
|
|
||||||
echo " 2. Drag and drop files from $RELEASE_DIR/"
|
|
||||||
echo " 3. Save"
|
|
||||||
echo ""
|
|
||||||
@@ -35,6 +35,10 @@ impl PortInfo {
|
|||||||
if self.port_name.contains("ttyUSB") || self.port_name.contains("ttyACM") {
|
if self.port_name.contains("ttyUSB") || self.port_name.contains("ttyACM") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// FreeBSD: cuaU* are USB serial ports
|
||||||
|
if self.port_name.contains("cuaU") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +91,18 @@ struct MatchingBoard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate serial ports via `arduino-cli board list --format json`.
|
/// Enumerate serial ports via `arduino-cli board list --format json`.
|
||||||
/// Falls back to OS-level detection if arduino-cli is unavailable.
|
/// Falls back to OS-level detection if arduino-cli is unavailable or returns
|
||||||
|
/// empty results (e.g. on FreeBSD where serial-discovery uses unsupported syscalls).
|
||||||
pub fn list_ports() -> Vec<PortInfo> {
|
pub fn list_ports() -> Vec<PortInfo> {
|
||||||
if let Some(cli) = find_arduino_cli() {
|
if let Some(cli) = find_arduino_cli() {
|
||||||
if let Ok(ports) = list_ports_via_cli(&cli) {
|
if let Ok(ports) = list_ports_via_cli(&cli) {
|
||||||
return ports;
|
if !ports.is_empty() {
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
// arduino-cli succeeded but found nothing -- on FreeBSD its
|
||||||
|
// serial-discovery subprocess uses Linux syscalls that are not
|
||||||
|
// implemented, so it always returns empty. Fall through to the
|
||||||
|
// OS-level scan which checks /dev/cuaU* directly.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
list_ports_fallback()
|
list_ports_fallback()
|
||||||
@@ -171,12 +182,22 @@ fn list_ports_fallback() -> Vec<PortInfo> {
|
|||||||
if let Ok(entries) = fs::read_dir("/dev") {
|
if let Ok(entries) = fs::read_dir("/dev") {
|
||||||
for entry in entries.flatten() {
|
for entry in entries.flatten() {
|
||||||
let name = entry.file_name().to_string_lossy().to_string();
|
let name = entry.file_name().to_string_lossy().to_string();
|
||||||
if name.starts_with("ttyUSB") || name.starts_with("ttyACM") {
|
// Linux: ttyUSB* (CH340/FTDI), ttyACM* (Arduino native USB)
|
||||||
|
// FreeBSD: cuaU* (USB serial)
|
||||||
|
// On FreeBSD, cuaU0.init and cuaU0.lock are control nodes,
|
||||||
|
// not actual ports -- only match bare cuaU* names.
|
||||||
|
let is_usb_port = name.starts_with("ttyUSB")
|
||||||
|
|| name.starts_with("ttyACM")
|
||||||
|
|| (name.starts_with("cuaU")
|
||||||
|
&& !name.contains('.'));
|
||||||
|
if is_usb_port {
|
||||||
let path = format!("/dev/{}", name);
|
let path = format!("/dev/{}", name);
|
||||||
let board = if name.starts_with("ttyUSB") {
|
let board = if name.starts_with("ttyUSB") {
|
||||||
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
|
"Likely CH340/FTDI (run 'anvil setup' for full detection)"
|
||||||
} else {
|
} else if name.starts_with("ttyACM") {
|
||||||
"Likely Arduino (run 'anvil setup' for full detection)"
|
"Likely Arduino (run 'anvil setup' for full detection)"
|
||||||
|
} else {
|
||||||
|
"USB serial device (run 'anvil setup' for full detection)"
|
||||||
};
|
};
|
||||||
result.push(PortInfo {
|
result.push(PortInfo {
|
||||||
port_name: path,
|
port_name: path,
|
||||||
@@ -306,14 +327,23 @@ pub fn print_port_details(ports: &[PortInfo]) {
|
|||||||
println!(" {}", "No serial devices found.".yellow());
|
println!(" {}", "No serial devices found.".yellow());
|
||||||
println!();
|
println!();
|
||||||
println!(" Checklist:");
|
println!(" Checklist:");
|
||||||
println!(" 1. Is the board plugged in via USB?");
|
|
||||||
println!(" 2. Is the USB cable a data cable (not charge-only)?");
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
{
|
||||||
println!(" 3. Check kernel log: dmesg | tail -20");
|
let mut n = 1u32;
|
||||||
println!(" 4. Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
|
println!(" {}. Is the board plugged in via USB?", n); n += 1;
|
||||||
|
println!(" {}. Is the USB cable a data cable (not charge-only)?", n); n += 1;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
println!(" {}. Check kernel log: dmesg | tail -20", n); n += 1;
|
||||||
|
println!(" {}. Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'", n); n += 1;
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
{
|
||||||
|
println!(" {}. Check kernel log: dmesg | grep -i usb", n); n += 1;
|
||||||
|
println!(" {}. List USB devices: usbconfig list", n); n += 1;
|
||||||
|
println!(" {}. Board should appear as /dev/cuaU0 (or cuaU1, cuaU2...)", n); n += 1;
|
||||||
|
}
|
||||||
|
println!(" {}. Try a different USB port or cable", n);
|
||||||
}
|
}
|
||||||
println!(" 5. Try a different USB port or cable");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,20 @@ pub fn scan_devices() -> Result<()> {
|
|||||||
let ports = board::list_ports();
|
let ports = board::list_ports();
|
||||||
board::print_port_details(&ports);
|
board::print_port_details(&ports);
|
||||||
|
|
||||||
// Also run arduino-cli board list for cross-reference
|
#[cfg(target_os = "freebsd")]
|
||||||
|
{
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Tip: if your board is not showing up, try plugging it directly into the machine -- some USB hubs prevent boards from being detected on FreeBSD.".bright_black()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also run arduino-cli board list for cross-reference.
|
||||||
|
// On FreeBSD, arduino-cli is a Linux binary whose serial-discovery subprocess
|
||||||
|
// uses syscalls not implemented in the Linux compat layer -- skip it entirely
|
||||||
|
// since we already detected ports via the OS fallback above.
|
||||||
|
#[cfg(not(target_os = "freebsd"))]
|
||||||
if let Some(cli_path) = board::find_arduino_cli() {
|
if let Some(cli_path) = board::find_arduino_cli() {
|
||||||
println!();
|
println!();
|
||||||
println!(
|
println!(
|
||||||
@@ -62,6 +75,14 @@ pub fn scan_devices() -> Result<()> {
|
|||||||
println!(" - Check kernel log: dmesg | tail -20");
|
println!(" - Check kernel log: dmesg | tail -20");
|
||||||
println!(" - Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
|
println!(" - Check USB bus: lsusb | grep -i -E 'ch34|arduino|1a86|2341'");
|
||||||
}
|
}
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
{
|
||||||
|
println!(" - Check kernel log: dmesg | grep -i usb");
|
||||||
|
println!(" - List USB devices: usbconfig list");
|
||||||
|
println!(" - Board should appear as /dev/cuaU0 (or cuaU1, cuaU2...)");
|
||||||
|
println!(" - Make sure you are in the dialer group: sudo pw groupmod dialer -m $USER");
|
||||||
|
println!(" - Then log out and back in for group change to take effect");
|
||||||
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
println!(" - Open Device Manager and check Ports (COM & LPT)");
|
println!(" - Open Device Manager and check Ports (COM & LPT)");
|
||||||
|
|||||||
@@ -10,7 +10,9 @@ pub struct SystemHealth {
|
|||||||
pub arduino_cli_path: Option<String>,
|
pub arduino_cli_path: Option<String>,
|
||||||
pub avr_core_ok: bool,
|
pub avr_core_ok: bool,
|
||||||
pub avr_size_ok: bool,
|
pub avr_size_ok: bool,
|
||||||
|
pub avr_size_path: Option<String>,
|
||||||
pub dialout_ok: bool,
|
pub dialout_ok: bool,
|
||||||
|
pub operator_ok: bool,
|
||||||
pub cmake_ok: bool,
|
pub cmake_ok: bool,
|
||||||
pub cpp_compiler_ok: bool,
|
pub cpp_compiler_ok: bool,
|
||||||
pub cpp_on_path: bool,
|
pub cpp_on_path: bool,
|
||||||
@@ -45,7 +47,8 @@ pub fn run_diagnostics(fix: bool) -> Result<()> {
|
|||||||
if fix {
|
if fix {
|
||||||
let optional_missing = !health.cmake_ok
|
let optional_missing = !health.cmake_ok
|
||||||
|| !health.cpp_compiler_ok
|
|| !health.cpp_compiler_ok
|
||||||
|| !health.git_ok;
|
|| !health.git_ok
|
||||||
|
|| !health.avr_size_ok;
|
||||||
if optional_missing {
|
if optional_missing {
|
||||||
println!();
|
println!();
|
||||||
run_fix_optional(&health)?;
|
run_fix_optional(&health)?;
|
||||||
@@ -87,8 +90,9 @@ pub fn check_system_health() -> SystemHealth {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let avr_size_ok = which::which("avr-size").is_ok();
|
let (avr_size_ok, avr_size_path) = find_avr_size();
|
||||||
let dialout_ok = check_dialout();
|
let dialout_ok = check_dialout();
|
||||||
|
let operator_ok = check_operator_group();
|
||||||
let cmake_ok = which::which("cmake").is_ok();
|
let cmake_ok = which::which("cmake").is_ok();
|
||||||
|
|
||||||
let cpp_on_path = which::which("g++").is_ok()
|
let cpp_on_path = which::which("g++").is_ok()
|
||||||
@@ -108,7 +112,9 @@ pub fn check_system_health() -> SystemHealth {
|
|||||||
arduino_cli_path,
|
arduino_cli_path,
|
||||||
avr_core_ok,
|
avr_core_ok,
|
||||||
avr_size_ok,
|
avr_size_ok,
|
||||||
|
avr_size_path,
|
||||||
dialout_ok,
|
dialout_ok,
|
||||||
|
operator_ok,
|
||||||
cmake_ok,
|
cmake_ok,
|
||||||
cpp_compiler_ok,
|
cpp_compiler_ok,
|
||||||
cpp_on_path,
|
cpp_on_path,
|
||||||
@@ -189,10 +195,11 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
"included with arduino:avr core (no separate install)".yellow()
|
"included with arduino:avr core (no separate install)".yellow()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
let hint = hint_avr_size_not_on_path(&health.avr_size_path);
|
||||||
println!(
|
println!(
|
||||||
" {} avr-size {}",
|
" {} avr-size {}",
|
||||||
"na".yellow(),
|
"na".yellow(),
|
||||||
hint_avr_size_not_on_path().yellow()
|
hint.as_str().yellow()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +214,20 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
" {} {} group {}",
|
" {} {} group {}",
|
||||||
"WARN".yellow(),
|
"WARN".yellow(),
|
||||||
group_name,
|
group_name,
|
||||||
format!("run: {}", fix_cmd).yellow()
|
format!("run: {} (then log out and back in)", fix_cmd).yellow()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
{
|
||||||
|
if health.operator_ok {
|
||||||
|
println!(" {} user in operator group (USB device access)", "ok".green());
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {} operator group {}",
|
||||||
|
"WARN".yellow(),
|
||||||
|
"run: sudo pw groupmod operator -m $USER (then log out and back in)".yellow()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,22 +274,39 @@ fn print_diagnostics(health: &SystemHealth) {
|
|||||||
|
|
||||||
if health.ports_found > 0 {
|
if health.ports_found > 0 {
|
||||||
println!(
|
println!(
|
||||||
" {} {} serial port(s) detected",
|
" {} {} Arduino board(s) detected",
|
||||||
"ok".green(),
|
"ok".green(),
|
||||||
health.ports_found
|
health.ports_found
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
let port_hint = if is_freebsd() {
|
// On FreeBSD, missing group membership prevents port detection entirely --
|
||||||
"(plug in a board to detect -- FreeBSD ports appear as /dev/cuaU0)"
|
// make that clear so the user doesn't chase a phantom hardware problem.
|
||||||
} else {
|
#[cfg(target_os = "freebsd")]
|
||||||
"(plug in a board to detect)"
|
{
|
||||||
};
|
if !health.dialout_ok || !health.operator_ok {
|
||||||
println!(
|
println!(
|
||||||
" {} no serial ports {}",
|
" {} no board detected {}",
|
||||||
"na".yellow(),
|
"na".yellow(),
|
||||||
port_hint.yellow()
|
"(cannot scan for boards until group access is fixed -- resolve the WARNs above and log out and back in)".yellow()
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
println!(
|
||||||
|
" {} no board detected {}",
|
||||||
|
"na".yellow(),
|
||||||
|
"(plug in your Arduino -- on FreeBSD boards appear as /dev/cuaU0)".yellow()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "freebsd"))]
|
||||||
|
{
|
||||||
|
println!(
|
||||||
|
" {} no board detected {}",
|
||||||
|
"na".yellow(),
|
||||||
|
"(plug in your Arduino to continue)".yellow()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_fix_instructions(health: &SystemHealth) {
|
fn print_fix_instructions(health: &SystemHealth) {
|
||||||
@@ -388,6 +425,16 @@ fn print_fix_instructions(health: &SystemHealth) {
|
|||||||
println!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
if !health.operator_ok {
|
||||||
|
println!(
|
||||||
|
" {}",
|
||||||
|
"Tip: add yourself to the operator group for USB device access: sudo pw groupmod operator -m $USER"
|
||||||
|
.bright_black()
|
||||||
|
);
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
if !health.git_ok {
|
if !health.git_ok {
|
||||||
println!(
|
println!(
|
||||||
" {}",
|
" {}",
|
||||||
@@ -618,6 +665,43 @@ fn run_fix(health: &SystemHealth) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- serial group (dialout / dialer) --
|
||||||
|
#[cfg(unix)]
|
||||||
|
if !health.dialout_ok {
|
||||||
|
let group = serial_group_name();
|
||||||
|
let fix_cmd = serial_group_fix_command();
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
format!("You are not in the {} group (needed for board upload).", group).bright_yellow()
|
||||||
|
);
|
||||||
|
if confirm(&format!("Add yourself to {} now?", group)) {
|
||||||
|
let parts: Vec<&str> = fix_cmd.split_whitespace().collect();
|
||||||
|
if run_cmd(parts[0], &parts[1..]) {
|
||||||
|
println!("{} Added to {} group. Log out and back in to apply.", "ok".green(), group);
|
||||||
|
} else {
|
||||||
|
println!("{} Failed. Run manually: {}", "FAIL".red(), fix_cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- operator group (FreeBSD USB device access) --
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
if !health.operator_ok {
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"You are not in the operator group (needed for USB device access on FreeBSD).".bright_yellow()
|
||||||
|
);
|
||||||
|
if confirm("Add yourself to operator group now?") {
|
||||||
|
if run_cmd("sudo", &["pw", "groupmod", "operator", "-m", &whoami()]) {
|
||||||
|
println!("{} Added to operator group. Log out and back in to apply.", "ok".green());
|
||||||
|
} else {
|
||||||
|
println!("{} Failed. Run manually: sudo pw groupmod operator -m $USER", "FAIL".red());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
run_fix_optional(health)?;
|
run_fix_optional(health)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -625,6 +709,52 @@ fn run_fix(health: &SystemHealth) -> Result<()> {
|
|||||||
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
||||||
let pm = detect_package_manager();
|
let pm = detect_package_manager();
|
||||||
|
|
||||||
|
// -- avr-size PATH fix --
|
||||||
|
if !health.avr_size_ok {
|
||||||
|
if let Some(ref bin_path) = health.avr_size_path {
|
||||||
|
let bin_dir = std::path::Path::new(bin_path)
|
||||||
|
.parent()
|
||||||
|
.map(|p| p.display().to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
println!();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
"avr-size is installed but not on PATH.".bright_yellow()
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
" Found at: {}",
|
||||||
|
bin_path.bright_cyan()
|
||||||
|
);
|
||||||
|
if !bin_dir.is_empty() {
|
||||||
|
let shell_rc = detect_shell_rc();
|
||||||
|
let export_line = format!(r#"export PATH="{}:$PATH""#, bin_dir);
|
||||||
|
if confirm(&format!("Add {} to PATH in {}?", bin_dir, shell_rc)) {
|
||||||
|
let rc_path = std::path::Path::new(&shell_rc);
|
||||||
|
let existing = std::fs::read_to_string(rc_path).unwrap_or_default();
|
||||||
|
if existing.contains(&export_line) {
|
||||||
|
println!("{} Already in {} -- log out and back in to apply.", "ok".green(), shell_rc);
|
||||||
|
} else {
|
||||||
|
let line = format!("
|
||||||
|
# Added by anvil doctor --fix
|
||||||
|
{}
|
||||||
|
", export_line);
|
||||||
|
if std::fs::OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.open(rc_path)
|
||||||
|
.and_then(|mut f| { use std::io::Write; f.write_all(line.as_bytes()) })
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
println!("{} Added to {}. Log out and back in to apply.", "ok".green(), shell_rc);
|
||||||
|
} else {
|
||||||
|
println!("{} Could not write to {}. Add manually:", "FAIL".red(), shell_rc);
|
||||||
|
println!(" {}", export_line.bright_cyan());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let items: Vec<(&str, bool, FixSpec)> = vec![
|
let items: Vec<(&str, bool, FixSpec)> = vec![
|
||||||
("cmake", health.cmake_ok, fix_spec_cmake(pm)),
|
("cmake", health.cmake_ok, fix_spec_cmake(pm)),
|
||||||
("C++ compiler", health.cpp_compiler_ok, fix_spec_cpp(pm)),
|
("C++ compiler", health.cpp_compiler_ok, fix_spec_cpp(pm)),
|
||||||
@@ -632,6 +762,9 @@ fn run_fix_optional(health: &SystemHealth) -> Result<()> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
let missing: Vec<_> = items.iter().filter(|(_, ok, _)| !ok).collect();
|
let missing: Vec<_> = items.iter().filter(|(_, ok, _)| !ok).collect();
|
||||||
|
if missing.is_empty() && health.avr_size_ok {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if missing.is_empty() {
|
if missing.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -795,14 +928,45 @@ fn fix_spec_git(pm: Option<&str>) -> FixSpec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hint_avr_size_not_on_path() -> &'static str {
|
fn hint_avr_size_not_on_path(found_path: &Option<String>) -> String {
|
||||||
|
let location = match found_path {
|
||||||
|
Some(p) => format!("found at {} -- add its directory to PATH, or log out and back in", p),
|
||||||
|
None => "not found -- install the AVR core first: anvil setup".to_string(),
|
||||||
|
};
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
"installed but not on PATH (binary size reports will be skipped)"
|
format!("installed but not on PATH ({})", location)
|
||||||
} else {
|
} else {
|
||||||
"installed but not on PATH"
|
format!("not on PATH ({})", location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Search common locations for avr-size and return (on_path, found_path).
|
||||||
|
fn find_avr_size() -> (bool, Option<String>) {
|
||||||
|
if which::which("avr-size").is_ok() {
|
||||||
|
return (true, None);
|
||||||
|
}
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
|
||||||
|
let search_roots = vec![
|
||||||
|
format!("{}/.arduino15/packages/arduino/tools/avr-gcc", home),
|
||||||
|
format!("{}/.local/share/arduino15/packages/arduino/tools/avr-gcc", home),
|
||||||
|
format!("{}\\AppData\\Local\\Arduino15\\packages\\arduino\\tools\\avr-gcc", home),
|
||||||
|
];
|
||||||
|
for root in &search_roots {
|
||||||
|
let root_path = std::path::Path::new(root);
|
||||||
|
if !root_path.exists() { continue; }
|
||||||
|
if let Ok(entries) = std::fs::read_dir(root_path) {
|
||||||
|
for version_entry in entries.flatten() {
|
||||||
|
let bin_name = if cfg!(target_os = "windows") { "avr-size.exe" } else { "avr-size" };
|
||||||
|
let bin = version_entry.path().join("bin").join(bin_name);
|
||||||
|
if bin.exists() {
|
||||||
|
return (false, Some(bin.display().to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(false, None)
|
||||||
|
}
|
||||||
|
|
||||||
fn hint_cmake() -> String {
|
fn hint_cmake() -> String {
|
||||||
if cfg!(target_os = "windows") {
|
if cfg!(target_os = "windows") {
|
||||||
"install: winget install Kitware.CMake (or choco install cmake)".into()
|
"install: winget install Kitware.CMake (or choco install cmake)".into()
|
||||||
@@ -855,6 +1019,7 @@ fn is_freebsd() -> bool {
|
|||||||
cfg!(target_os = "freebsd")
|
cfg!(target_os = "freebsd")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
fn serial_group_name() -> &'static str {
|
fn serial_group_name() -> &'static str {
|
||||||
if is_freebsd() {
|
if is_freebsd() {
|
||||||
"dialer"
|
"dialer"
|
||||||
@@ -863,6 +1028,7 @@ fn serial_group_name() -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
fn serial_group_fix_command() -> String {
|
fn serial_group_fix_command() -> String {
|
||||||
if is_freebsd() {
|
if is_freebsd() {
|
||||||
"sudo pw groupmod dialer -m $USER".to_string()
|
"sudo pw groupmod dialer -m $USER".to_string()
|
||||||
@@ -888,4 +1054,49 @@ fn check_dialout() -> bool {
|
|||||||
{
|
{
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
fn check_operator_group() -> bool {
|
||||||
|
#[cfg(target_os = "freebsd")]
|
||||||
|
{
|
||||||
|
match std::process::Command::new("groups").output() {
|
||||||
|
Ok(out) => {
|
||||||
|
let groups = String::from_utf8_lossy(&out.stdout);
|
||||||
|
groups.contains("operator")
|
||||||
|
}
|
||||||
|
Err(_) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "freebsd"))]
|
||||||
|
{
|
||||||
|
true // not applicable on other platforms
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn whoami() -> String {
|
||||||
|
std::process::Command::new("whoami")
|
||||||
|
.output()
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||||||
|
.unwrap_or_else(|_| "$USER".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect the user's shell RC file for PATH exports.
|
||||||
|
fn detect_shell_rc() -> String {
|
||||||
|
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
|
||||||
|
// Check SHELL env var first
|
||||||
|
let shell = std::env::var("SHELL").unwrap_or_default();
|
||||||
|
if shell.contains("zsh") {
|
||||||
|
return format!("{}/.zshrc", home);
|
||||||
|
}
|
||||||
|
if shell.contains("fish") {
|
||||||
|
return format!("{}/.config/fish/config.fish", home);
|
||||||
|
}
|
||||||
|
// Fall back to checking which RC files exist
|
||||||
|
let zshrc = format!("{}/.zshrc", home);
|
||||||
|
if std::path::Path::new(&zshrc).exists() {
|
||||||
|
return zshrc;
|
||||||
|
}
|
||||||
|
format!("{}/.bashrc", home)
|
||||||
}
|
}
|
||||||
@@ -306,6 +306,13 @@ pub fn run_refresh(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for filename in &has_changes {
|
||||||
|
println!(
|
||||||
|
" {} {} (updated)",
|
||||||
|
"ok".green(),
|
||||||
|
filename.bright_white()
|
||||||
|
);
|
||||||
|
}
|
||||||
println!(
|
println!(
|
||||||
"{} Updated {} file(s).",
|
"{} Updated {} file(s).",
|
||||||
"ok".green(),
|
"ok".green(),
|
||||||
|
|||||||
@@ -38,8 +38,29 @@ toml_get() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toml_array() {
|
toml_array() {
|
||||||
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \
|
# Handles both single-line: key = ["a", "b"]
|
||||||
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
# and multiline: key = [
|
||||||
|
# "a",
|
||||||
|
# "b",
|
||||||
|
# ]
|
||||||
|
awk -v key="$1" '
|
||||||
|
$0 ~ ("^" key " *= *\\[") {
|
||||||
|
collecting = 1
|
||||||
|
buf = $0
|
||||||
|
if (index($0, "]") > 0) { collecting = 0 }
|
||||||
|
}
|
||||||
|
collecting && NR > 1 { buf = buf " " $0 }
|
||||||
|
collecting && index($0, "]") > 0 && NR > 1 { collecting = 0 }
|
||||||
|
!collecting && buf != "" {
|
||||||
|
gsub(/.*\[/, "", buf)
|
||||||
|
gsub(/\].*/, "", buf)
|
||||||
|
gsub(/"/, "", buf)
|
||||||
|
gsub(/,/, " ", buf)
|
||||||
|
print buf
|
||||||
|
buf = ""
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$CONFIG" | tr -s " "
|
||||||
}
|
}
|
||||||
|
|
||||||
toml_section_get() {
|
toml_section_get() {
|
||||||
|
|||||||
@@ -39,8 +39,29 @@ toml_get() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toml_array() {
|
toml_array() {
|
||||||
(grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \
|
# Handles both single-line: key = ["a", "b"]
|
||||||
| sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' '
|
# and multiline: key = [
|
||||||
|
# "a",
|
||||||
|
# "b",
|
||||||
|
# ]
|
||||||
|
awk -v key="$1" '
|
||||||
|
$0 ~ ("^" key " *= *\\[") {
|
||||||
|
collecting = 1
|
||||||
|
buf = $0
|
||||||
|
if (index($0, "]") > 0) { collecting = 0 }
|
||||||
|
}
|
||||||
|
collecting && NR > 1 { buf = buf " " $0 }
|
||||||
|
collecting && index($0, "]") > 0 && NR > 1 { collecting = 0 }
|
||||||
|
!collecting && buf != "" {
|
||||||
|
gsub(/.*\[/, "", buf)
|
||||||
|
gsub(/\].*/, "", buf)
|
||||||
|
gsub(/"/, "", buf)
|
||||||
|
gsub(/,/, " ", buf)
|
||||||
|
print buf
|
||||||
|
buf = ""
|
||||||
|
exit
|
||||||
|
}
|
||||||
|
' "$CONFIG" | tr -s " "
|
||||||
}
|
}
|
||||||
|
|
||||||
toml_section_get() {
|
toml_section_get() {
|
||||||
@@ -69,8 +90,8 @@ LOCAL_CONFIG="$SCRIPT_DIR/.anvil.local"
|
|||||||
LOCAL_PORT=""
|
LOCAL_PORT=""
|
||||||
LOCAL_VID_PID=""
|
LOCAL_VID_PID=""
|
||||||
if [[ -f "$LOCAL_CONFIG" ]]; then
|
if [[ -f "$LOCAL_CONFIG" ]]; then
|
||||||
LOCAL_PORT="$(grep '^port ' "$LOCAL_CONFIG" 2>/dev/null | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ')"
|
LOCAL_PORT="$(grep '^port ' "$LOCAL_CONFIG" 2>/dev/null | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' || true)"
|
||||||
LOCAL_VID_PID="$(grep '^vid_pid ' "$LOCAL_CONFIG" 2>/dev/null | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ')"
|
LOCAL_VID_PID="$(grep '^vid_pid ' "$LOCAL_CONFIG" 2>/dev/null | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' || true)"
|
||||||
fi
|
fi
|
||||||
SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME"
|
SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME"
|
||||||
BUILD_DIR="$SCRIPT_DIR/.build"
|
BUILD_DIR="$SCRIPT_DIR/.build"
|
||||||
@@ -195,7 +216,7 @@ if [[ -z "$PORT" ]]; then
|
|||||||
PORT=$(arduino-cli board list 2>/dev/null \
|
PORT=$(arduino-cli board list 2>/dev/null \
|
||||||
| grep -i "serial" \
|
| grep -i "serial" \
|
||||||
| awk '{print $1}' \
|
| awk '{print $1}' \
|
||||||
| grep -E 'ttyUSB|ttyACM|COM' \
|
| grep -E 'ttyUSB|ttyACM|cuaU|COM' \
|
||||||
| head -1)
|
| head -1)
|
||||||
|
|
||||||
if [[ -z "$PORT" ]]; then
|
if [[ -z "$PORT" ]]; then
|
||||||
|
|||||||
19
xtask/Cargo.toml
Normal file
19
xtask/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "xtask"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
# CLI argument parsing
|
||||||
|
clap = { version = "4.4", features = ["derive"] }
|
||||||
|
# Error handling
|
||||||
|
anyhow = "1.0"
|
||||||
|
# Colors
|
||||||
|
colored = "2.1"
|
||||||
|
# Command existence check
|
||||||
|
which = "5.0"
|
||||||
667
xtask/src/main.rs
Normal file
667
xtask/src/main.rs
Normal file
@@ -0,0 +1,667 @@
|
|||||||
|
//! xtask -- build automation for Anvil.
|
||||||
|
//!
|
||||||
|
//! Usage:
|
||||||
|
//! cargo xtask Build all platform binaries
|
||||||
|
//! cargo xtask --check Check dependencies
|
||||||
|
//! cargo xtask --fix Install missing dependencies
|
||||||
|
//! cargo xtask --clean Remove zig/cargo cross-compile cache
|
||||||
|
//! cargo xtask --suffix rc1 Build with version suffix (e.g. 1.0.0-rc1)
|
||||||
|
//!
|
||||||
|
//! Produces:
|
||||||
|
//! release-artifacts/anvil-X.Y.Z-freebsd-x86_64.tar.gz (native)
|
||||||
|
//! release-artifacts/anvil-X.Y.Z-linux-x86_64.tar.gz (via cargo-zigbuild)
|
||||||
|
//! release-artifacts/anvil-X.Y.Z-windows-x86_64.zip (via cargo-zigbuild)
|
||||||
|
|
||||||
|
use anyhow::{bail, Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
use colored::*;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// CLI
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
name = "xtask",
|
||||||
|
about = "Build Anvil release binaries for all platforms",
|
||||||
|
long_about = "Builds FreeBSD (native), Linux, and Windows binaries using cargo-zigbuild.\n\
|
||||||
|
No VMs, no root, no containers required.\n\n\
|
||||||
|
First time: cargo xtask --fix\n\
|
||||||
|
Every release: cargo xtask"
|
||||||
|
)]
|
||||||
|
struct Args {
|
||||||
|
/// Check dependencies and report what is missing
|
||||||
|
#[arg(long)]
|
||||||
|
check: bool,
|
||||||
|
|
||||||
|
/// Install missing dependencies (zig, cargo-zigbuild, rustup targets)
|
||||||
|
#[arg(long)]
|
||||||
|
fix: bool,
|
||||||
|
|
||||||
|
/// Remove cargo target directories for cross-compile targets
|
||||||
|
#[arg(long)]
|
||||||
|
clean: bool,
|
||||||
|
|
||||||
|
/// Version suffix (e.g. rc1 -> 1.0.0-rc1)
|
||||||
|
#[arg(long, value_name = "SUFFIX")]
|
||||||
|
suffix: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Targets
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Target {
|
||||||
|
name: &'static str,
|
||||||
|
triple: &'static str,
|
||||||
|
binary_name: &'static str,
|
||||||
|
archive_ext: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
const TARGETS: &[Target] = &[
|
||||||
|
Target {
|
||||||
|
name: "Linux x86_64",
|
||||||
|
triple: "x86_64-unknown-linux-gnu",
|
||||||
|
binary_name: "anvil",
|
||||||
|
archive_ext: "tar.gz",
|
||||||
|
},
|
||||||
|
Target {
|
||||||
|
name: "Windows x86_64",
|
||||||
|
triple: "x86_64-pc-windows-gnu",
|
||||||
|
binary_name: "anvil.exe",
|
||||||
|
archive_ext: "zip",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn ok(msg: &str) {
|
||||||
|
println!("{} {}", "ok".green(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn warn(msg: &str) {
|
||||||
|
println!("{} {}", "warn".yellow(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn info(msg: &str) {
|
||||||
|
println!("{} {}", "....".cyan(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fail(msg: &str) {
|
||||||
|
eprintln!("{} {}", "FAIL".red(), msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn header(msg: &str) {
|
||||||
|
println!("\n{}\n", format!("=== {} ===", msg).bold());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cmd_exists(cmd: &str) -> bool {
|
||||||
|
which::which(cmd).is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(program: &str, args: &[&str], cwd: Option<&Path>) -> Result<()> {
|
||||||
|
let mut cmd = Command::new(program);
|
||||||
|
cmd.args(args);
|
||||||
|
if let Some(dir) = cwd {
|
||||||
|
cmd.current_dir(dir);
|
||||||
|
}
|
||||||
|
let status = cmd
|
||||||
|
.status()
|
||||||
|
.with_context(|| format!("Failed to run: {} {}", program, args.join(" ")))?;
|
||||||
|
if !status.success() {
|
||||||
|
bail!("{} {} failed with exit code: {}", program, args.join(" "), status);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_captured(program: &str, args: &[&str]) -> Result<String> {
|
||||||
|
let output = Command::new(program)
|
||||||
|
.args(args)
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.output()
|
||||||
|
.with_context(|| format!("Failed to run: {}", program))?;
|
||||||
|
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the workspace root.
|
||||||
|
/// When run via `cargo xtask`, cargo sets CARGO_MANIFEST_DIR to the xtask
|
||||||
|
/// package directory. The workspace root is one level up from there.
|
||||||
|
fn workspace_root() -> Result<PathBuf> {
|
||||||
|
// CARGO_MANIFEST_DIR points to xtask/ -- parent is the workspace root
|
||||||
|
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
|
||||||
|
let xtask_dir = PathBuf::from(manifest_dir);
|
||||||
|
if let Some(parent) = xtask_dir.parent() {
|
||||||
|
if parent.join("Cargo.toml").exists() {
|
||||||
|
return Ok(parent.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fallback: walk up from current dir looking for a workspace Cargo.toml
|
||||||
|
let mut dir = env::current_dir()?;
|
||||||
|
loop {
|
||||||
|
let cargo_toml = dir.join("Cargo.toml");
|
||||||
|
if cargo_toml.exists() {
|
||||||
|
// Check if it's a workspace (contains [workspace])
|
||||||
|
let contents = fs::read_to_string(&cargo_toml).unwrap_or_default();
|
||||||
|
if contents.contains("[workspace]") {
|
||||||
|
return Ok(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !dir.pop() {
|
||||||
|
bail!("Could not find workspace root (no Cargo.toml with [workspace] found)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read version from workspace Cargo.toml
|
||||||
|
fn read_version(workspace: &Path) -> Result<String> {
|
||||||
|
let cargo_toml = fs::read_to_string(workspace.join("Cargo.toml"))
|
||||||
|
.context("Failed to read Cargo.toml")?;
|
||||||
|
for line in cargo_toml.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
if line.starts_with("version") && line.contains('=') {
|
||||||
|
if let Some(v) = line.split('=').nth(1) {
|
||||||
|
let v = v.trim().trim_matches('"');
|
||||||
|
return Ok(v.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bail!("Could not read version from Cargo.toml")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Archive binary into release-artifacts/
|
||||||
|
fn package_binary(
|
||||||
|
workspace: &Path,
|
||||||
|
triple: &str,
|
||||||
|
binary_name: &str,
|
||||||
|
archive_name: &str,
|
||||||
|
archive_ext: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
let artifacts = workspace.join("release-artifacts");
|
||||||
|
fs::create_dir_all(&artifacts)?;
|
||||||
|
|
||||||
|
let binary_path = workspace
|
||||||
|
.join("target")
|
||||||
|
.join(triple)
|
||||||
|
.join("release")
|
||||||
|
.join(binary_name);
|
||||||
|
|
||||||
|
if !binary_path.exists() {
|
||||||
|
// Show what's actually in the target dir to help diagnose
|
||||||
|
let target_dir = binary_path.parent().unwrap();
|
||||||
|
let entries: Vec<String> = fs::read_dir(target_dir)
|
||||||
|
.map(|rd| rd.flatten()
|
||||||
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
.collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
bail!(
|
||||||
|
"Binary not found: {}\n Contents of {}:\n {}",
|
||||||
|
binary_path.display(),
|
||||||
|
target_dir.display(),
|
||||||
|
if entries.is_empty() { "(empty)".to_string() } else { entries.join(", ") }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
match archive_ext {
|
||||||
|
"tar.gz" => {
|
||||||
|
let archive = artifacts.join(format!("{}.tar.gz", archive_name));
|
||||||
|
run(
|
||||||
|
"tar",
|
||||||
|
&[
|
||||||
|
"-czf",
|
||||||
|
archive.to_str().unwrap(),
|
||||||
|
"-C",
|
||||||
|
binary_path.parent().unwrap().to_str().unwrap(),
|
||||||
|
binary_name,
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
"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,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => bail!("Unknown archive format: {}", archive_ext),
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Package native FreeBSD binary from target/release/
|
||||||
|
fn package_native(workspace: &Path, archive_name: &str) -> Result<()> {
|
||||||
|
let artifacts = workspace.join("release-artifacts");
|
||||||
|
fs::create_dir_all(&artifacts)?;
|
||||||
|
|
||||||
|
let binary_path = workspace.join("target").join("release").join("anvil");
|
||||||
|
if !binary_path.exists() {
|
||||||
|
bail!("Native binary not found: {}", binary_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let archive = artifacts.join(format!("{}.tar.gz", archive_name));
|
||||||
|
run(
|
||||||
|
"tar",
|
||||||
|
&[
|
||||||
|
"-czf",
|
||||||
|
archive.to_str().unwrap(),
|
||||||
|
"-C",
|
||||||
|
binary_path.parent().unwrap().to_str().unwrap(),
|
||||||
|
"anvil",
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate SHA256SUMS in release-artifacts/
|
||||||
|
fn generate_checksums(workspace: &Path) -> Result<()> {
|
||||||
|
let artifacts = workspace.join("release-artifacts");
|
||||||
|
let prev = env::current_dir()?;
|
||||||
|
env::set_current_dir(&artifacts)?;
|
||||||
|
|
||||||
|
// Collect artifact files
|
||||||
|
let mut files: Vec<String> = fs::read_dir(&artifacts)?
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|e| {
|
||||||
|
let name = e.file_name().to_string_lossy().to_string();
|
||||||
|
if name != "SHA256SUMS" {
|
||||||
|
Some(name)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
files.sort();
|
||||||
|
|
||||||
|
if cmd_exists("sha256sum") {
|
||||||
|
let mut args = vec!["--"];
|
||||||
|
for f in &files {
|
||||||
|
args.push(f.as_str());
|
||||||
|
}
|
||||||
|
let output = Command::new("sha256sum")
|
||||||
|
.args(&args)
|
||||||
|
.output()?;
|
||||||
|
fs::write("SHA256SUMS", &output.stdout)?;
|
||||||
|
} else if cmd_exists("shasum") {
|
||||||
|
let mut args = vec!["-a", "256", "--"];
|
||||||
|
for f in &files {
|
||||||
|
args.push(f.as_str());
|
||||||
|
}
|
||||||
|
let output = Command::new("shasum")
|
||||||
|
.args(&args)
|
||||||
|
.output()?;
|
||||||
|
fs::write("SHA256SUMS", &output.stdout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let checksums = fs::read_to_string("SHA256SUMS").unwrap_or_default();
|
||||||
|
print!("{}", checksums);
|
||||||
|
|
||||||
|
env::set_current_dir(prev)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Check
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn run_check(_workspace: &Path) -> Result<bool> {
|
||||||
|
header("Checking dependencies");
|
||||||
|
let mut missing = 0;
|
||||||
|
|
||||||
|
// zig
|
||||||
|
if cmd_exists("zig") {
|
||||||
|
let ver = run_captured("zig", &["version"]).unwrap_or_default();
|
||||||
|
ok(&format!("zig {}", ver));
|
||||||
|
} else {
|
||||||
|
warn("zig -- not found");
|
||||||
|
println!(" {}", "sudo pkg install zig (FreeBSD) or apt install zig (Linux)");
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// zip (for Windows archive)
|
||||||
|
if cmd_exists("zip") {
|
||||||
|
ok("zip");
|
||||||
|
} else {
|
||||||
|
warn("zip -- not found");
|
||||||
|
println!(" {}", "sudo pkg install zip (FreeBSD) or apt install zip (Linux)");
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// cargo-zigbuild
|
||||||
|
if cmd_exists("cargo-zigbuild") {
|
||||||
|
let ver = run_captured("cargo-zigbuild", &["--version"])
|
||||||
|
.unwrap_or_default();
|
||||||
|
ok(&format!("cargo-zigbuild {}", ver.replace("cargo-zigbuild ", "")));
|
||||||
|
} else {
|
||||||
|
warn("cargo-zigbuild -- not found");
|
||||||
|
println!(" {}", "cargo install cargo-zigbuild");
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// rustup targets
|
||||||
|
if cmd_exists("rustup") {
|
||||||
|
let targets = run_captured("rustup", &["target", "list", "--installed"])
|
||||||
|
.unwrap_or_default();
|
||||||
|
for t in TARGETS {
|
||||||
|
if targets.contains(t.triple) {
|
||||||
|
ok(&format!("rustup target: {}", t.triple));
|
||||||
|
} else {
|
||||||
|
warn(&format!("rustup target: {} -- not installed", t.triple));
|
||||||
|
println!(" rustup target add {}", t.triple);
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn("rustup -- not found");
|
||||||
|
println!(" {}", "curl https://sh.rustup.rs -sSf | sh");
|
||||||
|
missing += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host platform note
|
||||||
|
let host = host_os();
|
||||||
|
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.");
|
||||||
|
} else if host != "freebsd" {
|
||||||
|
println!();
|
||||||
|
warn(&format!("Unsupported host OS: {}", host));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
if missing == 0 {
|
||||||
|
ok("All dependencies satisfied -- ready to build!");
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
warn(&format!("{} dependency/dependencies missing. Run: cargo xtask --fix", missing));
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Fix
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn run_fix() -> Result<()> {
|
||||||
|
header("Installing dependencies");
|
||||||
|
|
||||||
|
let host = host_os();
|
||||||
|
|
||||||
|
// zig
|
||||||
|
if cmd_exists("zig") {
|
||||||
|
ok("zig already installed");
|
||||||
|
} else {
|
||||||
|
info("Installing zig...");
|
||||||
|
match host {
|
||||||
|
"freebsd" => run("sudo", &["pkg", "install", "-y", "zig"], None)?,
|
||||||
|
"linux" => {
|
||||||
|
// Try common package managers
|
||||||
|
if cmd_exists("apt-get") {
|
||||||
|
run("sudo", &["apt-get", "install", "-y", "zig"], None)?;
|
||||||
|
} else if cmd_exists("dnf") {
|
||||||
|
run("sudo", &["dnf", "install", "-y", "zig"], None)?;
|
||||||
|
} else {
|
||||||
|
bail!("Could not install zig automatically. Install manually: 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => bail!("Cannot auto-install zip on {}. Install it manually.", host),
|
||||||
|
}
|
||||||
|
ok("zip installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// cargo-zigbuild
|
||||||
|
if cmd_exists("cargo-zigbuild") {
|
||||||
|
ok("cargo-zigbuild already installed");
|
||||||
|
} else {
|
||||||
|
info("Installing cargo-zigbuild...");
|
||||||
|
run("cargo", &["install", "cargo-zigbuild"], None)?;
|
||||||
|
ok("cargo-zigbuild installed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// rustup targets
|
||||||
|
if cmd_exists("rustup") {
|
||||||
|
let installed = run_captured("rustup", &["target", "list", "--installed"])
|
||||||
|
.unwrap_or_default();
|
||||||
|
for t in TARGETS {
|
||||||
|
if installed.contains(t.triple) {
|
||||||
|
ok(&format!("rustup target {} already installed", t.triple));
|
||||||
|
} else {
|
||||||
|
info(&format!("Adding rustup target {}...", t.triple));
|
||||||
|
run("rustup", &["target", "add", t.triple], None)?;
|
||||||
|
ok(&format!("rustup target {} added", t.triple));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bail!("rustup not found -- install it first: curl https://sh.rustup.rs -sSf | sh");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
ok("All dependencies installed. Run: cargo xtask");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Clean
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn run_clean(workspace: &Path) -> Result<()> {
|
||||||
|
header("Cleaning cross-compile artifacts");
|
||||||
|
|
||||||
|
for t in TARGETS {
|
||||||
|
let target_dir = workspace.join("target").join(t.triple);
|
||||||
|
if target_dir.exists() {
|
||||||
|
info(&format!("Removing target/{}/...", t.triple));
|
||||||
|
fs::remove_dir_all(&target_dir)?;
|
||||||
|
ok(&format!("target/{} removed", t.triple));
|
||||||
|
} else {
|
||||||
|
ok(&format!("target/{} already clean", t.triple));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let artifacts = workspace.join("release-artifacts");
|
||||||
|
if artifacts.exists() {
|
||||||
|
info("Removing release-artifacts/...");
|
||||||
|
fs::remove_dir_all(&artifacts)?;
|
||||||
|
ok("release-artifacts removed");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
ok("Clean complete.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Build
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn run_build(workspace: &Path, version: &str) -> Result<()> {
|
||||||
|
let host = host_os();
|
||||||
|
|
||||||
|
println!();
|
||||||
|
println!("{}", format!("Building Anvil v{} -- all platforms", version).bold());
|
||||||
|
println!(" Version: {} (from Cargo.toml)", version);
|
||||||
|
println!(" Host: {}", host);
|
||||||
|
|
||||||
|
// Clean previous artifacts
|
||||||
|
let artifacts = workspace.join("release-artifacts");
|
||||||
|
if artifacts.exists() {
|
||||||
|
fs::remove_dir_all(&artifacts)?;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
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!();
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- 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));
|
||||||
|
|
||||||
|
// -- Checksums ---------------------------------------------------------
|
||||||
|
header("Checksums");
|
||||||
|
generate_checksums(workspace)?;
|
||||||
|
|
||||||
|
// -- Summary -----------------------------------------------------------
|
||||||
|
println!();
|
||||||
|
println!("{}", format!("=== Anvil v{} ready ===", version).bold().green());
|
||||||
|
println!();
|
||||||
|
for entry in fs::read_dir(&artifacts)?.flatten() {
|
||||||
|
let meta = entry.metadata()?;
|
||||||
|
println!(
|
||||||
|
" {} ({:.1} MB)",
|
||||||
|
entry.file_name().to_string_lossy(),
|
||||||
|
meta.len() as f64 / 1_048_576.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
println!("Upload to Gitea:");
|
||||||
|
println!(" 1. Releases -> New Release -> Tag: v{}", version);
|
||||||
|
println!(" 2. Drag and drop everything from release-artifacts/");
|
||||||
|
println!(" 3. Save");
|
||||||
|
println!();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let args = Args::parse();
|
||||||
|
let workspace = workspace_root().expect("Could not find workspace root");
|
||||||
|
|
||||||
|
let result = if args.clean {
|
||||||
|
run_clean(&workspace)
|
||||||
|
} else if args.check {
|
||||||
|
match run_check(&workspace) {
|
||||||
|
Ok(true) => Ok(()),
|
||||||
|
Ok(false) => std::process::exit(1),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
} else if args.fix {
|
||||||
|
run_fix()
|
||||||
|
} else {
|
||||||
|
// Build -- check deps first
|
||||||
|
match run_check(&workspace) {
|
||||||
|
Ok(false) => {
|
||||||
|
println!();
|
||||||
|
fail("Dependencies missing.");
|
||||||
|
fail("Run: cargo xtask --fix");
|
||||||
|
println!();
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
fail(&format!("Dependency check failed: {}", e));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Ok(true) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let base_version =
|
||||||
|
read_version(&workspace).expect("Could not read version from Cargo.toml");
|
||||||
|
let version = match &args.suffix {
|
||||||
|
Some(s) => format!("{}-{}", base_version, s.trim_start_matches('v')),
|
||||||
|
None => base_version,
|
||||||
|
};
|
||||||
|
|
||||||
|
run_build(&workspace, &version)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = result {
|
||||||
|
fail(&format!("{:#}", e));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user