use anyhow::{bail, Context, Result};
use std::path::Path;
use std::process::Command;
/// Build cross-compilation toolchain
pub fn build_toolchain(build_dir: &Path) -> Result<()> {
let toolchain_dir = build_dir.join("toolchain");
std::fs::create_dir_all(&toolchain_dir)?;
// Check if we already have the toolchain
let cross_gcc = format!("{}-gcc", "aarch64-linux-gnu");
if which::which(&cross_gcc).is_ok() {
println!("Cross-compiler already installed: {}", cross_gcc);
return Ok(());
}
println!("Installing cross-compilation toolchain...");
// Detect package manager at runtime
let has_pacman = which::which("pacman").is_ok();
let has_apt = which::which("apt").is_ok();
let has_dnf = which::which("dnf").is_ok();
let has_brew = which::which("brew").is_ok();
if has_pacman {
// Arch Linux / Manjaro
run_cmd("sudo", &["pacman", "-S", "--needed", "aarch64-linux-gnu-gcc"], build_dir)?;
} else if has_apt {
// Debian / Ubuntu
run_cmd("sudo", &["apt", "install", "-y", "gcc-aarch64-linux-gnu"], build_dir)?;
} else if has_dnf {
// Fedora
run_cmd("sudo", &["dnf", "install", "-y", "gcc-aarch64-linux-gnu"], build_dir)?;
} else if has_brew {
// macOS with Homebrew
run_cmd("brew", &["install", "aarch64-elf-gcc"], build_dir)?;
} else {
println!("Warning: Could not detect package manager");
println!("Please install cross-compiler manually:");
println!(" Arch: sudo pacman -S aarch64-linux-gnu-gcc");
println!(" Ubuntu: sudo apt install gcc-aarch64-linux-gnu");
println!(" Fedora: sudo dnf install gcc-aarch64-linux-gnu");
}
// Install Rust cross-compilation target
println!("Installing Rust aarch64 target...");
run_cmd("rustup", &["target", "add", "aarch64-unknown-linux-gnu"], build_dir)?;
// Install cargo tools for cross-compilation
println!("Installing cargo-cross...");
run_cmd("cargo", &["install", "cross"], build_dir)?;
println!("Toolchain installation complete!");
Ok(())
}
/// Ensure the current process is running as root, re-executing the already
/// compiled binary through `sudo` when necessary.
///
/// Never call `cargo` under sudo: rustup toolchains are per-user and root has
/// no default, so `sudo cargo` fails with "could not choose a version of
/// cargo". Re-executing `current_exe()` sidesteps that entirely.
pub fn require_root(action: &str) -> Result<()> {
if is_root() {
return Ok(());
}
if std::env::var_os("ALARM_NO_ELEVATE").is_some() || which::which("sudo").is_err() {
bail!(
"Root privileges are required to {}.\n\
Run it with sudo, or set ALARM_NO_ELEVATE=1 to see this message.",
action
);
}
println!("Root privileges required to {}; re-running under sudo...", action);
let exe = std::env::current_exe().context("failed to locate the current executable")?;
let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect();
let status = Command::new("sudo")
.arg(&exe)
.args(&args)
.status()
.context("failed to run sudo")?;
std::process::exit(status.code().unwrap_or(1));
}
fn is_root() -> bool {
Command::new("id")
.arg("-u")
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).trim() == "0")
.unwrap_or(false)
}
/// Hand generated files back to the user who invoked `sudo`, so later
/// non-root commands can overwrite them.
pub fn restore_ownership(dirs: &[std::path::PathBuf]) {
let (Ok(uid), Ok(gid)) = (std::env::var("SUDO_UID"), std::env::var("SUDO_GID")) else {
return;
};
for dir in dirs {
if dir.exists() {
let _ = Command::new("chown")
.args(["-R", &format!("{uid}:{gid}")])
.arg(dir)
.status();
}
}
}
/// Flash image to SD card
pub fn flash_image(image_path: &Path, device: &Path) -> Result<()> {
if !image_path.exists() {
bail!("Image not found: {}", image_path.display());
}
require_root("write to a block device")?;
// Refuse to write to anything that is not a block device.
let meta = device
.metadata()
.with_context(|| format!("cannot access {}", device.display()))?;
if !is_block_device(&meta) {
bail!(
"{} is not a block device. Pass the whole card, e.g. /dev/sdX.",
device.display()
);
}
println!("Flashing image to {}...", device.display());
println!("Warning: This will destroy all data on {}", device.display());
println!("Press Ctrl+C to cancel, or wait 5 seconds to continue...");
std::thread::sleep(std::time::Duration::from_secs(5));
// Best-effort: unmount any auto-mounted partitions of the target.
unmount_partitions(device);
let status = Command::new("dd")
.arg(format!("if={}", image_path.display()))
.arg(format!("of={}", device.display()))
.args(["bs=4M", "status=progress", "conv=fsync"])
.status()
.context("Failed to run dd")?;
if !status.success() {
bail!("Flash failed");
}
Command::new("sync").status()?;
// Re-read the partition table so the new filesystems are usable.
let _ = Command::new("partprobe").arg(device).status();
println!("Flash complete!");
println!("Remove the SD card and insert it into your Switch.");
println!("Boot into hekate and select 'More configs...' -> 'L4T ALARM'.");
Ok(())
}
#[cfg(unix)]
fn is_block_device(meta: &std::fs::Metadata) -> bool {
use std::os::unix::fs::FileTypeExt;
meta.file_type().is_block_device()
}
#[cfg(not(unix))]
fn is_block_device(_meta: &std::fs::Metadata) -> bool {
false
}
/// `umount device*`-style cleanup. Best effort: ignore failures.
fn unmount_partitions(device: &Path) {
if let Ok(out) = Command::new("lsblk").args(["-lnpo", "NAME"]).arg(device).output() {
for name in String::from_utf8_lossy(&out.stdout).lines() {
let name = name.trim();
if !name.is_empty() {
let _ = Command::new("umount").arg(name).status();
}
}
}
}
/// Check system dependencies
#[allow(dead_code)]
pub fn check_dependencies() -> Result<()> {
let deps = [
("git", "Version control"),
("make", "Build tool"),
("gcc", "C compiler"),
("flex", "Lexer generator"),
("bison", "Parser generator"),
("dtc", "Device tree compiler"),
("bc", "Calculator"),
("rsync", "File sync"),
("parted", "Partition editor"),
("dosfstools", "FAT filesystem tools"),
("e2fsprogs", "ext4 tools"),
];
let mut missing = Vec::new();
for (cmd, desc) in &deps {
if which::which(cmd).is_err() {
missing.push(format!(" {} - {}", cmd, desc));
}
}
if !missing.is_empty() {
println!("Missing dependencies:");
for dep in &missing {
println!("{}", dep);
}
println!("\nInstall with:");
println!(" Arch: sudo pacman -S git make gcc flex bison dtc bc rsync parted dosfstools e2fsprogs");
println!(" Ubuntu: sudo apt install git make gcc flex bison device-tree-compiler bc rsync parted dosfstools e2fsprogs");
} else {
println!("All dependencies satisfied!");
}
Ok(())
}
/// Run a command
fn run_cmd(program: &str, args: &[&str], work_dir: &Path) -> Result<()> {
let status = Command::new(program)
.args(args)
.current_dir(work_dir)
.status()
.context(format!("Failed to execute {}", program))?;
if !status.success() {
anyhow::bail!("{} failed with status: {}", program, status);
}
Ok(())
}