#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![allow(clippy::cast_possible_truncation)]
#![allow(clippy::cast_possible_wrap)]
#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_precision_loss)]
mod app;
mod block;
mod config;
mod firmware;
mod pipeline;
mod screens;
mod secureboot;
mod steps;
mod subproc;
mod util;
use std::path::PathBuf;
use std::process::Command;
use anyhow::{Context as _, Result, bail};
use argh::FromArgs;
use crate::app::Installer;
use crate::config::Config;
use crate::pipeline::Context;
#[derive(FromArgs)]
struct Args {
#[argh(option)]
config: Option<PathBuf>,
#[argh(option)]
drive: Option<String>,
#[argh(option)]
source_image: Option<PathBuf>,
#[argh(option, default = "1")]
start_from: usize,
#[argh(option)]
skip: Option<String>,
#[argh(option)]
k3s_token_file: Option<PathBuf>,
}
fn parse_step_list(value: &str, step_count: usize) -> Result<Vec<usize>> {
let mut steps = Vec::new();
for item in value.split(',') {
let item = item.trim();
if item.is_empty() {
continue;
}
let n: usize = item.parse().with_context(|| {
format!("--skip expects comma-separated step numbers, got '{value}'")
})?;
if n < 1 || n > step_count {
bail!("--skip values must be between 1 and {step_count}: {n}");
}
steps.push(n);
}
Ok(steps)
}
fn read_first_line(path: &std::path::Path) -> Result<String> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read {}", path.display()))?;
Ok(content.lines().next().unwrap_or("").to_string())
}
fn main() -> Result<()> {
let args: Args = argh::from_env();
if unsafe { libc::geteuid() } != 0 {
eprintln!("puu-installer must be run as root.");
std::process::exit(1);
}
let config_path = args
.config
.or_else(|| std::env::var_os("PUU_INSTALLER_CONFIG").map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("/usr/share/puu/machine.yaml"));
let cfg = Config::load(&config_path)?;
let source_image = match args.source_image {
Some(p) => Some(p),
None => pipeline::default_source_image(&cfg),
};
if source_image.is_none() && !pipeline::running_in_container() {
eprintln!(
"no local bootc source image found; expected \
/dev/disk/by-label/{} or --source-image.",
cfg.image.source_label
);
std::process::exit(1);
}
let step_count = pipeline::STEPS.len();
if args.start_from < 1 || args.start_from > step_count {
eprintln!("--start-from must be between 1 and {step_count}");
std::process::exit(2);
}
let skip_steps: Vec<usize> = match &args.skip {
Some(s) => parse_step_list(s, step_count)?,
None => Vec::new(),
};
let k3s_token = match &args.k3s_token_file {
Some(path) => read_first_line(path)?,
None => String::new(),
};
if cfg.k3s.role != "server" && cfg.k3s.role != "agent" {
eprintln!(
"k3s.role must be \"server\" or \"agent\", got \"{}\".",
cfg.k3s.role
);
std::process::exit(1);
}
if cfg.k3s.role == "agent" && (cfg.k3s.server_url.is_empty() || k3s_token.is_empty()) {
eprintln!("k3s.role agent requires k3s.serverUrl and --k3s-token-file.");
std::process::exit(1);
}
let target_imgref = pipeline::load_target_imgref(source_image.as_deref(), &cfg);
let image_version = pipeline::load_image_version(source_image.as_deref());
let ctx = Context {
config: cfg,
source_image,
target_imgref,
image_version,
drive: args.drive.unwrap_or_default(),
drive_label: String::new(),
username: String::new(),
user_password: String::new(),
user_shell: "/bin/bash".to_string(),
keymap: String::new(),
locale: String::new(),
secure_boot: true,
k3s_token,
last_error: String::new(),
start_from: args.start_from,
skip_steps,
parts: pipeline::DerivedPartitions::default(),
};
let mut app = Installer::new(ctx);
loop {
match app.run() {
app::Trap::Quit => return Ok(()),
app::Trap::Reboot => {
unsafe { libc::sync() };
let _ = nix::sys::reboot::reboot(nix::sys::reboot::RebootMode::RB_AUTOBOOT);
bail!("reboot syscall failed");
}
app::Trap::Shell => {
let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
let _ = Command::new(&shell).status();
app = Installer::new(app.into_context());
}
}
}
}