use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::time::Duration;
use anyhow::{Context as _, Result, bail};
use efivar::boot::{
BootEntry, BootEntryAttributes, EFIHardDrive, EFIHardDriveType, FilePath, FilePathList,
};
use crate::image;
use crate::partition;
use crate::pipeline::{
self, BOOTC_IMAGE_STORAGE, BOOTC_RUNROOT, BOOTC_STORAGE_MOUNT, BOOTC_VAR_TMP,
CONTAINERS_RUNROOT, DEPENDENCIES, Runner, Step, TARGET_EFI_MOUNT, TARGET_HOME_MOUNT,
TARGET_MOUNT,
};
fn step_dependencies(r: &mut Runner) -> Result<()> {
let missing: Vec<&str> = DEPENDENCIES
.iter()
.copied()
.filter(|d| !crate::util::which_exists(d))
.collect();
if !missing.is_empty() {
r.log("missing required tools:");
for dep in &missing {
r.log(&format!(" {dep}"));
}
bail!("missing dependencies: {}", missing.join(", "));
}
r.log(&format!("all {} dependencies present", DEPENDENCIES.len()));
Ok(())
}
fn step_setup_mode(r: &mut Runner) -> Result<()> {
if !r.ctx.secure_boot {
r.log("Secure Boot disabled; skipping firmware setup check");
return Ok(());
}
let status = crate::firmware::detect_setup_mode();
if status.setup_mode.is_none() {
r.ctx.secure_boot = false;
r.log(
"Secure Boot Setup Mode could not be detected; \
continuing without Secure Boot",
);
if !status.reason.is_empty() {
r.log(&format!(" reason: {}", status.reason));
}
return Ok(());
}
if status.setup_mode == Some(false) {
bail!(
"Secure Boot Setup Mode is off. Reboot to firmware setup or \
continue without Secure Boot."
);
}
let state = if status.secure_boot == Some(true) {
"enabled"
} else {
"disabled"
};
r.log(&format!(
"firmware reports Setup Mode on; Secure Boot is {state}"
));
Ok(())
}
fn step_cleanup(r: &mut Runner) -> Result<()> {
pipeline::cleanup_mounts(r)
}
fn step_validate_drive(r: &mut Runner) -> Result<()> {
let drive = r.ctx.drive.clone();
if !Path::new(&drive).exists() {
bail!("'{drive}' is not a block device");
}
if let Some(ref src) = r.ctx.source_image {
if !src.is_file() && !src.is_dir() {
bail!("source bootc image not found at {}", src.display());
}
}
match crate::block::disk_busy(&drive) {
Some(crate::block::BusyReason::Mounted { ident, mountpoint }) => {
bail!("'{drive}' is busy: {ident} mounted at {mountpoint}");
}
Some(crate::block::BusyReason::Mapping { kind, ident }) => {
bail!("'{drive}' has active {kind} mapping {ident}");
}
None => {}
}
let size_b = crate::block::drive_size_bytes(&drive)?;
let min = partition::min_target_size_bytes(&r.ctx.config)?;
if size_b < min {
bail!(
"'{drive}' is too small: {} detected, need at least {} for EFI, \
root and home partitions",
crate::util::human_size(size_b),
crate::util::human_size(min)
);
}
r.ctx.parts = partition::derive_partitions(&drive);
r.log(&format!(
"target {drive} ({}; {}, {}, {})",
crate::util::human_size(size_b),
r.ctx.parts.efi_part,
r.ctx.parts.root_part,
r.ctx.parts.home_part
));
Ok(())
}
fn step_partition(r: &mut Runner) -> Result<()> {
let drive = r.ctx.drive.clone();
let efi = r.ctx.config.partitions.efi.clone();
let home = r.ctx.config.partitions.home.clone();
r.ctx.parts.layout = Some(partition::partition_drive(r, &drive, &efi, &home)?);
if crate::util::which_exists("blockdev") {
let _ = r.cmd(&["blockdev", "--rereadpt", &drive]).run();
}
if crate::util::which_exists("udevadm") {
let _ = r.cmd(&["udevadm", "settle", "--timeout=30"]).run();
}
pipeline::wait_for_block_device(r, &r.ctx.parts.efi_part.clone())?;
pipeline::wait_for_block_device(r, &r.ctx.parts.root_part.clone())?;
pipeline::wait_for_block_device(r, &r.ctx.parts.home_part.clone())?;
Ok(())
}
fn step_format_efi(r: &mut Runner) -> Result<()> {
let part = r.ctx.parts.efi_part.clone();
pipeline::wait_for_block_device(r, &part)?;
r.log(&format!("formatting {part} as FAT32 (label EFI)"));
format_efi_fat(&part)?;
r.ctx.parts.efi_uuid = crate::block::capture_partuuid(&part)?;
Ok(())
}
fn format_efi_fat(part: &str) -> Result<()> {
let mut label = [b' '; 11];
label[..3].copy_from_slice(b"EFI");
let mut volume_id = [0u8; 4];
openssl::rand::rand_bytes(&mut volume_id)?;
let mut device = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(part)
.map_err(|e| anyhow::anyhow!("failed to open {part}: {e}"))?;
let options = fatfs::FormatVolumeOptions::new()
.fat_type(fatfs::FatType::Fat32)
.volume_id(u32::from_le_bytes(volume_id))
.volume_label(label);
fatfs::format_volume(&mut device, options)
.map_err(|e| anyhow::anyhow!("failed to write FAT32 filesystem to {part}: {e}"))?;
device.sync_all()?;
Ok(())
}
fn step_format_root(r: &mut Runner) -> Result<()> {
let part = r.ctx.parts.root_part.clone();
pipeline::wait_for_block_device(r, &part)?;
r.log(&format!("formatting {part} as XFS (label puu-root)"));
format_root_xfs(&part)?;
r.ctx.parts.root_uuid = crate::block::capture_uuid(&part)?;
Ok(())
}
fn format_root_xfs(part: &str) -> Result<()> {
let mut uuid = [0u8; 16];
openssl::rand::rand_bytes(&mut uuid)?;
let mut options = mkfs_xfs::FormatOptions::from_target(part, uuid)
.map_err(|e| anyhow::anyhow!("failed to inspect {part} for XFS formatting: {e}"))?;
options.label = Some("puu-root".into());
let plan = mkfs_xfs::FormatPlan::new(options)
.map_err(|e| anyhow::anyhow!("invalid XFS geometry for {part}: {e}"))?;
let mut device = std::fs::OpenOptions::new()
.write(true)
.open(part)
.map_err(|e| anyhow::anyhow!("failed to open {part}: {e}"))?;
mkfs_xfs::image::write_plan_to(&plan, &mut device)
.map_err(|e| anyhow::anyhow!("failed to write XFS metadata to {part}: {e}"))?;
device.sync_all()?;
Ok(())
}
fn step_format_home(r: &mut Runner) -> Result<()> {
let part = r.ctx.parts.home_part.clone();
pipeline::wait_for_block_device(r, &part)?;
r.cmd(&[
"mke2fs", "-t", "ext4", "-O", "encrypt", "-F", "-L", "puu-home", &part,
])
.run()?;
r.ctx.parts.home_uuid = crate::block::capture_uuid(&part)?;
Ok(())
}
fn mount_efi(r: &mut Runner) -> Result<()> {
let efi_part = r.ctx.parts.efi_part.clone();
pipeline::mount_target(
r,
&efi_part,
TARGET_EFI_MOUNT,
"vfat",
Some("fmask=0077,dmask=0077"),
)
}
fn step_mount(r: &mut Runner) -> Result<()> {
let root_part = r.ctx.parts.root_part.clone();
pipeline::mount_target(r, &root_part, TARGET_MOUNT, "xfs", None)
}
fn step_mount_home(r: &mut Runner) -> Result<()> {
let home_part = r.ctx.parts.home_part.clone();
pipeline::mount_target(r, &home_part, TARGET_HOME_MOUNT, "ext4", None)?;
Ok(())
}
#[allow(clippy::too_many_lines)]
fn step_bootc_install(r: &mut Runner) -> Result<()> {
let source_image = r.ctx.source_image.clone();
let source_image = if source_image.is_none() {
let default = image::default_source_image(&r.ctx.config);
r.ctx.source_image.clone_from(&default);
if let Some(ref img) = default {
r.ctx.target_imgref = image::load_target_imgref(Some(img), &r.ctx.config);
r.ctx.image_version = image::load_image_version(Some(img));
r.log(&format!(
"using local bootc image source: {}",
img.display()
));
}
default
} else {
source_image
};
if source_image.is_none() && !image::running_in_container() {
bail!(
"no local bootc source image found; expected \
/dev/disk/by-label/{}",
r.ctx.config.image.source_label
);
}
let mut mounted_bootc_tmp = false;
let mut remove_bootc_storage = false;
let mut mounted_bootc_image_storage = false;
let mut mounted_bootc_tmp_dir = false;
let result = (|| -> Result<()> {
if !Path::new(BOOTC_STORAGE_MOUNT).exists() {
std::fs::create_dir_all(BOOTC_STORAGE_MOUNT)?;
remove_bootc_storage = true;
}
for dir in &[BOOTC_IMAGE_STORAGE, CONTAINERS_RUNROOT, BOOTC_RUNROOT] {
std::fs::create_dir_all(dir)?;
}
let bootc_tmp_dir = PathBuf::from(BOOTC_STORAGE_MOUNT).join("tmp");
std::fs::create_dir_all(&bootc_tmp_dir)?;
if !pipeline::path_is_mount(BOOTC_IMAGE_STORAGE) {
pipeline::bind_mount(BOOTC_IMAGE_STORAGE, BOOTC_IMAGE_STORAGE)?;
mounted_bootc_image_storage = true;
}
if !pipeline::path_is_mount(&bootc_tmp_dir) {
let bootc_tmp_dir = bootc_tmp_dir.to_string_lossy();
pipeline::bind_mount(&bootc_tmp_dir, &bootc_tmp_dir)?;
mounted_bootc_tmp_dir = true;
}
mounted_bootc_tmp = pipeline::mount_bootc_vartmp()?;
let storage_conf = PathBuf::from("/run/puu-installer/bootc-storage.conf");
std::fs::write(
&storage_conf,
format!(
"[storage]\ndriver = \"overlay\"\ngraphroot = \"{BOOTC_IMAGE_STORAGE}\"\n\
runroot = \"{CONTAINERS_RUNROOT}\"\n"
),
)?;
let mut argv: Vec<String> = vec!["bootc".into(), "install".into(), "to-filesystem".into()];
if let Some(ref src) = source_image {
argv.push("--source-imgref".into());
argv.push(image::image_source_ref(src));
}
argv.extend([
"--target-imgref".into(),
r.ctx.target_imgref.clone(),
"--skip-fetch-check".into(),
"--bootloader".into(),
"none".into(),
"--skip-finalize".into(),
]);
for arg in pipeline::install_kernel_args(&r.ctx.parts.root_uuid, &r.ctx.config) {
argv.push("--karg".into());
argv.push(arg);
}
argv.push(TARGET_MOUNT.into());
r.log("bootc image deployment may take several minutes without native progress output");
r.progress(0.05, "Install bootc image");
let env = vec![
(
"CONTAINERS_STORAGE_CONF".into(),
storage_conf.to_string_lossy().to_string(),
),
("TMPDIR".into(), BOOTC_VAR_TMP.into()),
];
r.cmd(&argv)
.env(&env)
.heartbeat(
Duration::from_secs(30),
"bootc install still running; deploying image",
)
.run()?;
Ok(())
})();
if mounted_bootc_tmp {
let _ = pipeline::unmount(BOOTC_VAR_TMP);
}
if mounted_bootc_tmp_dir {
let bootc_tmp_dir = PathBuf::from(BOOTC_STORAGE_MOUNT).join("tmp");
let _ = pipeline::unmount(&bootc_tmp_dir.to_string_lossy());
}
if mounted_bootc_image_storage {
let _ = pipeline::unmount(BOOTC_IMAGE_STORAGE);
}
if remove_bootc_storage {
let _ = std::fs::remove_dir_all(BOOTC_STORAGE_MOUNT);
}
result?;
r.progress(0.95, "Install bootc image");
Ok(())
}
fn step_configure_mounts(r: &mut Runner) -> Result<()> {
let deploy = pipeline::deployment_root()?;
for dir in &["efi", "var", "var/home"] {
std::fs::create_dir_all(deploy.join(dir))?;
}
let etc = deploy.join("etc");
std::fs::create_dir_all(&etc)?;
let fstab = etc.join("fstab");
let old = if fstab.exists() {
std::fs::read_to_string(&fstab)?
} else {
String::new()
};
let new = pipeline::merge_fstab(&old, &r.ctx.parts.efi_uuid, &r.ctx.parts.home_uuid);
std::fs::write(&fstab, &new)?;
std::fs::set_permissions(&fstab, PermissionsExt::from_mode(0o644))?;
Ok(())
}
fn read_archive_entry(src: &Path, name: &Path) -> Result<Option<Vec<u8>>> {
let file = std::fs::File::open(src)?;
let mut archive = tar::Archive::new(file);
for entry in archive.entries()? {
let mut entry = entry?;
if entry.path()?.as_ref() == name {
let mut data = Vec::new();
std::io::Read::read_to_end(&mut entry, &mut data)?;
return Ok(Some(data));
}
}
Ok(None)
}
fn oci_index_manifest_digest(index: &[u8]) -> Result<String> {
let json: serde_json::Value = serde_json::from_slice(index)?;
json.get("manifests")
.and_then(|m| m.as_array())
.and_then(|m| m.first())
.and_then(|m| m.get("digest"))
.and_then(|d| d.as_str())
.map(str::to_string)
.ok_or_else(|| anyhow::anyhow!("OCI index.json has no manifest digest"))
}
fn oci_blob_path(digest: &str) -> Result<PathBuf> {
let (algo, hex) = digest
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("malformed OCI digest {digest}"))?;
Ok(Path::new("blobs").join(algo).join(hex))
}
fn step_verify_image(r: &mut Runner) -> Result<()> {
let Some(src) = r.ctx.source_image.clone() else {
r.log("using running bootc image as install source");
return Ok(());
};
let manifest = image::image_manifest_metadata(&src);
if manifest.is_null() {
r.log("no image manifest found, skipping digest verification");
return Ok(());
}
let expected = manifest
.get("digest")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if expected.is_empty() {
r.log("manifest has no digest entry, skipping");
return Ok(());
}
r.log(&format!("expected image digest: {expected}"));
let raw = if src.is_dir() {
let manifest_path = src.join("manifest.json");
std::fs::read(&manifest_path).map_err(|e| {
anyhow::anyhow!(
"failed to read image manifest {}: {e}",
manifest_path.display()
)
})?
} else if let Some(data) = read_archive_entry(&src, Path::new("manifest.json"))? {
data
} else {
let index = read_archive_entry(&src, Path::new("index.json"))?.ok_or_else(|| {
anyhow::anyhow!(
"image archive {} has neither manifest.json nor index.json",
src.display()
)
})?;
let digest = oci_index_manifest_digest(&index)?;
let blob = oci_blob_path(&digest)?;
read_archive_entry(&src, &blob)?.ok_or_else(|| {
anyhow::anyhow!(
"OCI archive {} is missing manifest blob {}",
src.display(),
blob.display()
)
})?
};
let actual = format!("sha256:{}", hex::encode(openssl::sha::sha256(&raw)));
if actual != expected {
bail!("image digest mismatch: expected {expected}, got {actual}");
}
r.log(&format!("image digest verified: {actual}"));
Ok(())
}
fn validate_timezone(deploy: &Path, timezone: &str) -> Result<()> {
let relative = Path::new(timezone);
if !relative
.components()
.all(|component| matches!(component, std::path::Component::Normal(_)))
{
bail!("invalid timezone: {timezone}");
}
let path = deploy.join("usr/share/zoneinfo").join(relative);
if !path.is_file() {
bail!("timezone not found in target system: {timezone}");
}
Ok(())
}
fn step_firstboot(r: &mut Runner) -> Result<()> {
let ident = &r.ctx.config.identity;
let keymap = if r.ctx.keymap.is_empty() {
ident.keymap.clone()
} else {
r.ctx.keymap.clone()
};
let locale = if r.ctx.locale.is_empty() {
ident.locale.clone()
} else {
r.ctx.locale.clone()
};
let timezone = ident.timezone.clone();
let hostname = ident.hostname.clone();
if !hostname.is_empty() && !crate::util::valid_hostname(&hostname) {
bail!("invalid hostname: {hostname}");
}
let deploy = pipeline::deployment_root()?;
if !timezone.is_empty() {
validate_timezone(&deploy, &timezone)?;
}
let etc = deploy.join("etc");
std::fs::create_dir_all(&etc)?;
let write_if_absent = |path: PathBuf, contents: String| -> Result<()> {
if !path.exists() {
std::fs::write(&path, contents)?;
std::fs::set_permissions(&path, PermissionsExt::from_mode(0o644))?;
}
Ok(())
};
if !hostname.is_empty() {
write_if_absent(etc.join("hostname"), format!("{hostname}\n"))?;
}
if !locale.is_empty() {
write_if_absent(etc.join("locale.conf"), format!("LANG={locale}\n"))?;
}
if !timezone.is_empty() {
let link = etc.join("localtime");
if !link.exists() {
std::os::unix::fs::symlink(format!("../usr/share/zoneinfo/{timezone}"), &link)?;
}
}
if !keymap.is_empty() {
let vconsole = etc.join("vconsole.conf");
std::fs::write(&vconsole, format!("KEYMAP={keymap}\n"))?;
std::fs::set_permissions(&vconsole, PermissionsExt::from_mode(0o644))?;
r.log(&format!("staged keymap {keymap} in /etc/vconsole.conf"));
}
Ok(())
}
fn step_configure_k3s_cluster(r: &mut Runner) -> Result<()> {
if r.ctx.config.k3s.role == "server" && r.ctx.config.k3s.node_name.is_empty() {
r.log("k3s cluster: using image default server configuration");
return Ok(());
}
let mut argv: Vec<String> = vec![
r.ctx.config.k3s.helper.clone(),
"--role".into(),
r.ctx.config.k3s.role.clone(),
];
if !r.ctx.config.k3s.server_url.is_empty() {
argv.push("--server-url".into());
argv.push(r.ctx.config.k3s.server_url.clone());
}
if !r.ctx.k3s_token.is_empty() {
let token_path = Path::new(TARGET_MOUNT).join("etc/rancher/k3s/puu-token");
{
let parent = token_path
.parent()
.ok_or_else(|| anyhow::anyhow!("token path has no parent"))?;
std::fs::create_dir_all(parent)?;
}
std::fs::write(&token_path, format!("{}\n", r.ctx.k3s_token))?;
std::fs::set_permissions(&token_path, PermissionsExt::from_mode(0o600))?;
argv.push("--token-file".into());
argv.push("/etc/rancher/k3s/puu-token".into());
}
if !r.ctx.config.k3s.node_name.is_empty() {
argv.push("--node-name".into());
argv.push(r.ctx.config.k3s.node_name.clone());
}
r.cmd(&argv).chroot(TARGET_MOUNT).run()?;
r.log(&format!(
"k3s cluster: staged {} configuration",
r.ctx.config.k3s.role
));
Ok(())
}
fn step_stage_homed_identity(r: &mut Runner) -> Result<()> {
if r.ctx.username.is_empty() {
bail!("username is required to stage a homed identity");
}
let name = &r.ctx.username;
if r.ctx.user_password.contains('\n') || r.ctx.user_password.contains('\r') {
bail!("password must not contain newline characters");
}
let hashed = crate::passwd::hash_password_sha512(&r.ctx.user_password)?;
let identity = serde_json::json!({
"userName": name,
"secret": { "password": [&r.ctx.user_password] },
"realName": name,
"shell": r.ctx.user_shell,
"memberOf": r.ctx.config.homed.groups.as_slice(),
"homeDirectory": format!("/var/home/{name}"),
"imagePath": format!("/var/home/{name}.homedir"),
"storage": "fscrypt",
"preferredSessionLauncher": "gnome",
"enforcePasswordPolicy": false,
"privileged": {
"hashedPassword": [&hashed],
"password": [&r.ctx.user_password],
},
});
let deployment = pipeline::deployment_root()?;
let ostree_deploy_dir = deployment.parent().and_then(Path::parent).ok_or_else(|| {
anyhow::anyhow!(
"deployment root has unexpected path: {}",
deployment.display()
)
})?;
let target = ostree_deploy_dir.join("var/lib/puu/identity.json");
{
let parent = target
.parent()
.ok_or_else(|| anyhow::anyhow!("identity target path has no parent"))?;
std::fs::create_dir_all(parent)?;
}
std::fs::write(&target, serde_json::to_string_pretty(&identity)?)?;
std::fs::set_permissions(&target, PermissionsExt::from_mode(0o600))?;
r.log(&format!(
"staged homed identity for {name} at /var/lib/puu/identity.json"
));
Ok(())
}
fn step_flatpak_seed(r: &mut Runner) -> Result<()> {
let manifest =
Path::new(TARGET_MOUNT).join(r.ctx.config.flatpak.manifest_path.trim_start_matches('/'));
let refs = pipeline::flatpak_manifest_refs(&manifest);
if refs.is_empty() {
r.log("flatpak-seed: no Flatpak refs in manifest");
return Ok(());
}
let sync =
Path::new(TARGET_MOUNT).join(r.ctx.config.flatpak.sync_helper.trim_start_matches('/'));
if !sync.is_file() {
r.log("flatpak-seed: flatpak-sync helper missing; skipping");
return Ok(());
}
r.log(&format!(
"flatpak-seed: installing {} system Flatpak refs",
refs.len()
));
let mounted = pipeline::mount_chroot_binds()?;
let sync_helper = r.ctx.config.flatpak.sync_helper.clone();
let result = r
.cmd(&[sync_helper])
.chroot(TARGET_MOUNT)
.heartbeat(
Duration::from_secs(30),
"flatpak pre-seed still running; downloading apps",
)
.run();
pipeline::unmount_chroot_binds(&mounted)?;
result?;
r.log(
"flatpak-seed: system Flatpaks installed; system flatpak-sync.timer \
will keep them reconciled",
);
Ok(())
}
fn step_nvram(r: &mut Runner) -> Result<()> {
mount_efi(r)?;
let source = systemd_boot_source()?;
let arch = efi_arch_suffix()?;
let systemd_dir = Path::new(TARGET_EFI_MOUNT).join("EFI/systemd");
let fallback_dir = Path::new(TARGET_EFI_MOUNT).join("EFI/BOOT");
std::fs::create_dir_all(&systemd_dir)?;
std::fs::create_dir_all(&fallback_dir)?;
let systemd_target = systemd_dir.join(format!("systemd-boot{arch}.efi"));
let fallback_target = fallback_dir.join(format!("BOOT{arch}.EFI"));
std::fs::copy(&source, &systemd_target)?;
std::fs::copy(&source, &fallback_target)?;
if r.ctx.parts.layout.is_none() {
r.ctx.parts.layout = Some(partition::load_partition_layout(&r.ctx.drive)?);
}
let layout = r
.ctx
.parts
.layout
.as_ref()
.ok_or_else(|| anyhow::anyhow!("EFI partition layout metadata is missing"))?;
let partition_sig = uuid::Builder::from_bytes_le(layout.efi.unique_guid).into_uuid();
let file_path_list = FilePathList {
hard_drive: EFIHardDrive {
partition_number: layout.efi.number,
partition_start: layout.efi.starting_lba,
partition_size: layout.efi.ending_lba - layout.efi.starting_lba + 1,
partition_sig,
format: 2,
sig_type: EFIHardDriveType::Gpt,
},
file_path: FilePath {
path: format!(r"\EFI\systemd\systemd-boot{arch}.efi"),
},
};
let entry = BootEntry {
attributes: BootEntryAttributes::LOAD_OPTION_ACTIVE,
description: "puu".into(),
file_path_list: Some(file_path_list),
optional_data: Vec::new(),
};
let mut vars = efivar::system();
let old_order = vars.get_boot_order().unwrap_or_default();
let boot_id = first_free_boot_id(&old_order, vars.as_ref())?;
vars.add_boot_entry(boot_id, entry)
.map_err(|e| anyhow::anyhow!("failed to write Boot{boot_id:04X}: {e}"))?;
let mut new_order = vec![boot_id];
new_order.extend(old_order.into_iter().filter(|id| *id != boot_id));
vars.set_boot_order(new_order)
.map_err(|e| anyhow::anyhow!("failed to update BootOrder: {e}"))?;
r.log(&format!(
"installed systemd-boot and registered Boot{boot_id:04X}"
));
Ok(())
}
fn systemd_boot_source() -> Result<PathBuf> {
let dirs = [
Path::new(TARGET_MOUNT).join("usr/lib/systemd/boot/efi"),
PathBuf::from("/usr/lib/systemd/boot/efi"),
];
for dir in dirs {
if !dir.is_dir() {
continue;
}
for entry in
std::fs::read_dir(&dir).with_context(|| format!("failed to read {}", dir.display()))?
{
let entry = entry?;
let path = entry.path();
let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
continue;
};
let has_efi_extension = Path::new(name)
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("efi"));
if name.starts_with("systemd-boot") && has_efi_extension {
return Ok(path);
}
}
}
bail!("systemd-boot EFI binary not found")
}
fn efi_arch_suffix() -> Result<&'static str> {
match std::env::consts::ARCH {
"x86_64" => Ok("x64"),
"aarch64" => Ok("aa64"),
"arm" => Ok("arm"),
arch => bail!("unsupported EFI architecture: {arch}"),
}
}
fn first_free_boot_id(order: &[u16], vars: &dyn efivar::VarManager) -> Result<u16> {
for id in 0..=u16::MAX {
if order.contains(&id) {
continue;
}
let name = efivar::efi::Variable::new(&format!("Boot{id:04X}"));
if vars.read(&name).is_err() {
return Ok(id);
}
}
bail!("no free Boot#### variable ID found")
}
fn step_stage_esp_boot(r: &mut Runner) -> Result<()> {
mount_efi(r)?;
let source_entries = Path::new(TARGET_MOUNT).join("boot/loader/entries");
let source_ostree = Path::new(TARGET_MOUNT).join("boot/ostree");
if !source_entries.is_dir() {
bail!(
"boot loader entries not found: {}",
source_entries.display()
);
}
if !source_ostree.is_dir() {
bail!("boot ostree files not found: {}", source_ostree.display());
}
let esp_entries = Path::new(TARGET_EFI_MOUNT).join("loader/entries");
let esp_ostree = Path::new(TARGET_EFI_MOUNT).join("boot/ostree");
if esp_entries.exists() {
std::fs::remove_dir_all(&esp_entries)?;
}
std::fs::create_dir_all(&esp_entries)?;
{
let parent = esp_ostree
.parent()
.ok_or_else(|| anyhow::anyhow!("esp ostree path has no parent"))?;
std::fs::create_dir_all(parent)?;
}
for entry in std::fs::read_dir(&source_entries)? {
let entry = entry?;
if entry.path().is_file() {
std::fs::copy(entry.path(), esp_entries.join(entry.file_name()))?;
}
}
pipeline::copy_tree_files(&source_ostree, &esp_ostree)?;
unsafe {
libc::sync();
}
r.log("staged boot loader entries and ostree kernel files on ESP");
Ok(())
}
fn step_secureboot(r: &mut Runner) -> Result<()> {
crate::secureboot::ensure_sbctl_keys(&mut r.log)?;
let keydir = crate::secureboot::sbctl_keydir();
let owner = crate::secureboot::sbctl_owner_guid();
match crate::secureboot::enroll_keys(&keydir, &owner) {
Ok(()) => {
r.log("enrolled Secure Boot PK/KEK/db keys");
Ok(())
}
Err(e) => {
if crate::firmware::detect_setup_mode().setup_mode == Some(false) {
r.log("Secure Boot keys appear to be enrolled already");
Ok(())
} else {
Err(e)
}
}
}
}
fn step_sign(r: &mut Runner) -> Result<()> {
crate::secureboot::ensure_sbctl_keys(&mut r.log)?;
let efi_binaries = crate::secureboot::target_efi_binaries();
if efi_binaries.is_empty() {
bail!("no EFI binaries found under {TARGET_EFI_MOUNT}");
}
let keydir = crate::secureboot::sbctl_keydir();
let db_key = std::fs::read_to_string(keydir.join("db/db.key"))?;
let db_cert = std::fs::read_to_string(keydir.join("db/db.pem"))?;
let mut signed_count = 0usize;
let mut skipped_count = 0usize;
for binary in &efi_binaries {
let image = std::fs::read(binary)?;
if crate::secureboot::verify_pe(&image, &db_cert)
.with_context(|| format!("failed to verify signature for {}", binary.display()))?
{
skipped_count += 1;
continue;
}
let signed = crate::secureboot::sign_pe(&image, &db_key, &db_cert)?;
std::fs::write(binary, &signed)?;
signed_count += 1;
}
r.log(&format!(
"signed {signed_count} EFI binaries on target ESP ({skipped_count} already signed)"
));
for binary in &efi_binaries {
let image = std::fs::read(binary)?;
if !crate::secureboot::verify_pe(&image, &db_cert)? {
bail!("signature verification failed for {}", binary.display());
}
}
Ok(())
}
fn step_final_cleanup(r: &mut Runner) -> Result<()> {
pipeline::cleanup_mounts(r)?;
r.log("target filesystems unmounted; ready to reboot");
Ok(())
}
pub static STEPS: &[Step] = &[
Step {
title: "Verify dependencies",
fn_: step_dependencies,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Verify Secure Boot setup",
fn_: step_setup_mode,
point_of_no_return: false,
requires_secure_boot: true,
},
Step {
title: "Clean up previous mounts",
fn_: step_cleanup,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Validate target drive",
fn_: step_validate_drive,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Verify image integrity",
fn_: step_verify_image,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Partition drive",
fn_: step_partition,
point_of_no_return: true,
requires_secure_boot: false,
},
Step {
title: "Format EFI partition",
fn_: step_format_efi,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Format root filesystem",
fn_: step_format_root,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Format home filesystem",
fn_: step_format_home,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Mount target filesystems",
fn_: step_mount,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Install bootc image",
fn_: step_bootc_install,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Mount home filesystem",
fn_: step_mount_home,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Configure persistent mounts",
fn_: step_configure_mounts,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Configure firstboot identity",
fn_: step_firstboot,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Configure k3s cluster",
fn_: step_configure_k3s_cluster,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Stage homed user identity",
fn_: step_stage_homed_identity,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Pre-stage Flatpak refs",
fn_: step_flatpak_seed,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Manage NVRAM boot entry",
fn_: step_nvram,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Stage ESP boot files",
fn_: step_stage_esp_boot,
point_of_no_return: false,
requires_secure_boot: false,
},
Step {
title: "Sign EFI binaries",
fn_: step_sign,
point_of_no_return: false,
requires_secure_boot: true,
},
Step {
title: "Enroll Secure Boot keys",
fn_: step_secureboot,
point_of_no_return: false,
requires_secure_boot: true,
},
Step {
title: "Unmount target filesystems",
fn_: step_final_cleanup,
point_of_no_return: false,
requires_secure_boot: false,
},
];