1use std::borrow::Cow;
9use std::ffi::OsStr;
10use std::ffi::OsString;
11use std::fmt::Display;
12use std::fmt::Write as _;
13use std::io::Write;
14use std::num::NonZeroU64;
15use std::path::Path;
16use std::process::Command;
17use std::process::Stdio;
18
19use anyhow::Ok;
20use anyhow::{Context, Result};
21use bootc_blockdev::Device;
22use bootc_utils::CommandRunExt;
23use camino::Utf8Path;
24use camino::Utf8PathBuf;
25use cap_std::fs::Dir;
26use cap_std_ext::cap_std;
27use clap::ValueEnum;
28use fn_error_context::context;
29use serde::{Deserialize, Serialize};
30
31use super::AbootDiskLayout;
32use super::BOOT;
33use super::MountSpec;
34use super::RUN_BOOTC;
35use super::RW_KARG;
36use super::RootSetup;
37use super::State;
38use super::config::Filesystem;
39use crate::bootloader::systemd_version;
40use crate::task::Task;
41use bootc_blockdev::dps::BIOS_BOOT;
42#[cfg(feature = "install-to-disk")]
43use bootc_mount::is_mounted_in_pid1_mountns;
44use linux_kernel_cmdline::utf8::Cmdline;
45
46#[cfg(feature = "install-to-disk")]
57fn use_discoverable_partitions(state: &State) -> bool {
58 if let Some(ref config) = state.install_config {
60 if let Some(v) = config.discoverable_partitions {
61 return v;
62 }
63 }
64 matches!(
66 state.config_opts.bootloader,
67 Some(crate::spec::Bootloader::Systemd) | Some(crate::spec::Bootloader::GrubCC)
68 )
69}
70
71pub(crate) const BOOTPN_SIZE_MB: u32 = 510;
73pub(crate) const EFIPN_SIZE_MB: u32 = 512;
76pub(crate) const CFS_EFIPN_SIZE_MB: u32 = 2048;
80const ABOOT_BOOT_SIZE_MB: u32 = 192;
81const ABOOT_VBMETA_SIZE_MB: u32 = 64;
82const UKIBOOT_ESP_SIZE_MB: u32 = 100;
83const UKIBOOT_CTL_SIZE_MB: u32 = 1;
84pub(crate) const ABOOT_BOOT_GUID: &str = "20117f86-e985-4357-b9ee-374bc1d8487d";
85pub(crate) const ABOOT_VBMETA_GUID: &str = "4b7a15d6-322c-42ac-8110-88b7da0c5d77";
86pub(crate) const UKIBOOT_BOOT_GUID: &str = "df331e4d-be00-463f-b4a7-8b43e18fb53a";
87const UKIBOOT_CTL_GUID: &str = "fefd9070-346f-4c9a-85e6-17f07f922773";
88#[cfg(feature = "install-to-disk")]
89pub(crate) const PREPBOOT_GUID: &str = "9E1A2D38-C612-4316-AA26-8B49521E5A8B";
90#[cfg(feature = "install-to-disk")]
91pub(crate) const PREPBOOT_LABEL: &str = "PowerPC-PReP-boot";
92
93#[derive(clap::ValueEnum, Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
94#[serde(rename_all = "kebab-case")]
95pub(crate) enum BlockSetup {
96 #[default]
97 Direct,
98 Tpm2Luks,
99}
100
101impl Display for BlockSetup {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 self.to_possible_value().unwrap().get_name().fmt(f)
104 }
105}
106
107#[derive(Debug, Clone, clap::Args, Serialize, Deserialize, PartialEq, Eq)]
109#[serde(rename_all = "kebab-case")]
110pub(crate) struct InstallBlockDeviceOpts {
111 pub(crate) device: Utf8PathBuf,
113
114 #[clap(long)]
116 #[serde(default)]
117 pub(crate) wipe: bool,
118
119 #[clap(long, value_enum)]
124 pub(crate) block_setup: Option<BlockSetup>,
125
126 #[clap(long, value_enum)]
128 pub(crate) filesystem: Option<Filesystem>,
129
130 #[clap(long)]
135 pub(crate) root_size: Option<String>,
136}
137
138impl BlockSetup {
139 pub(crate) fn requires_bootpart(&self) -> bool {
141 match self {
142 BlockSetup::Direct => false,
143 BlockSetup::Tpm2Luks => true,
144 }
145 }
146}
147
148#[cfg(feature = "install-to-disk")]
149fn mkfs<'a>(
150 dev: &str,
151 fs: Filesystem,
152 label: &str,
153 wipe: bool,
154 opts: impl IntoIterator<Item = &'a str>,
155) -> Result<uuid::Uuid> {
156 let devinfo = bootc_blockdev::list_dev(dev.into())?;
157 let size = ostree_ext::glib::format_size(devinfo.size);
158
159 let u = uuid::Uuid::new_v4();
161
162 let mut t = Task::new(
163 &format!("Creating {label} filesystem ({fs}) on device {dev} (size={size})"),
164 format!("mkfs.{fs}"),
165 );
166 match fs {
167 Filesystem::Xfs => {
168 if wipe {
169 t.cmd.arg("-f");
170 }
171 t.cmd.arg("-m");
172 t.cmd.arg(format!("uuid={u}"));
173 }
174 Filesystem::Btrfs | Filesystem::Ext4 => {
175 t.cmd.arg("-U");
176 t.cmd.arg(u.to_string());
177 }
178 };
179 t.cmd.args(["-L", label]);
181 t.cmd.args(opts);
182 t.cmd.arg(dev);
183 t.cmd.stdout(Stdio::null());
185 t.verbose().run()?;
187 Ok(u)
188}
189
190pub(crate) fn wipefs(dev: &Utf8Path) -> Result<()> {
191 println!("Wiping device {dev}");
192 Command::new("wipefs")
193 .args(["-a", dev.as_str()])
194 .run_inherited_with_cmd_context()
195}
196
197pub(crate) fn udev_settle() -> Result<()> {
198 std::thread::sleep(std::time::Duration::from_millis(200));
203
204 let st = super::run_in_host_mountns("udevadm")?
205 .arg("settle")
206 .status()?;
207 if !st.success() {
208 anyhow::bail!("Failed to run udevadm settle: {st:?}");
209 }
210 Ok(())
211}
212
213#[derive(Debug)]
215struct PartitionLayout {
216 esp_partno: Option<u32>,
217 boot_partno: Option<u32>,
218 rootpn: u32,
219 used_repart: bool,
221}
222
223#[derive(Debug, Deserialize)]
225struct RepartPartition {
226 #[serde(rename = "type")]
229 partition_type: String,
230 #[serde(default)]
231 label: Option<String>,
232 #[serde(default)]
236 partno: Option<u32>,
237 #[serde(default)]
238 raw_size: u64,
239 #[allow(dead_code)]
240 fs: Option<String>,
241 file: String,
243}
244
245fn has_systemd_repart() -> bool {
246 if Command::new("systemd-repart")
247 .arg("--help")
248 .stdout(Stdio::null())
249 .stderr(Stdio::null())
250 .status()
251 .is_err()
252 {
253 return false;
254 }
255
256 true
257}
258
259fn can_use_systemd_repart() -> bool {
260 if !has_systemd_repart() {
261 return false;
262 }
263
264 let has_config = REPART_CONFIG_DIRS.iter().map(Path::new).any(|d| {
265 d.is_dir()
266 && d.read_dir()
267 .ok()
268 .is_some_and(|mut entries| entries.next().is_some())
269 });
270
271 has_config
272}
273
274const REPART_INCLUDE_PARTITIONS_MIN_VERSION: u32 = 253;
276
277const REPART_MKFS_OPTIONS_MIN_VERSION: u32 = 254;
281
282const REPART_CONFIG_DIRS: &[&str] = &[
286 "/etc/repart.d",
287 "/run/repart.d",
288 "/usr/local/lib/repart.d",
289 "/usr/lib/repart.d",
290];
291
292fn generated_root_definition_name() -> String {
298 format!("50-root-{}.conf", uuid::Uuid::new_v4())
299}
300
301const REPART_PLANNED_SIZE_DROPIN: &str = "zz-bootc-planned-size.conf";
305
306const MIB: u64 = 1024 * 1024;
308
309fn need_filtered_definitions(generic_image: bool) -> Result<bool> {
312 Ok(generic_image && systemd_version()? < REPART_INCLUDE_PARTITIONS_MIN_VERSION)
313}
314
315fn repart_partition_is_required(part: &RepartPartition) -> bool {
318 let ptype = part.partition_type.as_str();
319 ptype.starts_with("root")
320 || ptype.starts_with("esp")
321 || ptype == "bios"
322 || ptype.eq_ignore_ascii_case(BIOS_BOOT)
323}
324
325fn collect_repart_definitions(
331 dry_partitions: &[RepartPartition],
332 dest_dir: &Path,
333 filter: impl Fn(&RepartPartition) -> bool,
334) -> Result<()> {
335 std::fs::create_dir_all(dest_dir)
336 .with_context(|| format!("Creating {}", dest_dir.display()))?;
337
338 for part in dry_partitions {
339 if !filter(part) {
340 continue;
341 }
342
343 let src = REPART_CONFIG_DIRS
346 .iter()
347 .map(|d| Path::new(d).join(&part.file))
348 .find(|p| p.exists())
349 .ok_or_else(|| anyhow::anyhow!("Could not find repart.d definition {}", part.file))?;
350 let name = src
351 .file_name()
352 .context("Missing repart.d definition filename")?;
353
354 std::fs::copy(&src, dest_dir.join(name))
355 .with_context(|| format!("Copying repart.d definition {}", src.display()))?;
356 }
357
358 Ok(())
359}
360
361fn planned_root_size_mib(planned: &[RepartPartition]) -> Result<NonZeroU64> {
367 let root = planned
368 .iter()
369 .find(|p| p.partition_type.starts_with("root-"))
370 .ok_or_else(|| anyhow::anyhow!("systemd-repart planned no root partition"))?;
371 NonZeroU64::new(root.raw_size / MIB).ok_or_else(|| {
372 anyhow::anyhow!("systemd-repart planned less than 1 MiB for the root partition")
373 })
374}
375
376fn planned_boot_partition_sizes(planned: &[RepartPartition]) -> Result<Vec<(String, u64)>> {
384 planned
385 .iter()
386 .filter(|p| repart_partition_is_required(p) && !p.partition_type.starts_with("root"))
387 .map(|p| {
388 let name = Utf8Path::new(&p.file)
389 .file_name()
390 .ok_or_else(|| anyhow::anyhow!("Invalid repart.d definition path {}", p.file))?;
391 Ok((name.to_owned(), p.raw_size))
392 })
393 .collect()
394}
395
396fn planned_size_pin(size: u64) -> String {
398 format!("[Partition]\nSizeMinBytes={size}\nSizeMaxBytes={size}\n")
399}
400
401fn write_repart_file(dir: &Path, name: &str, conf: &str) -> Result<()> {
404 let path = dir.join(name);
405 if let Some(parent) = path.parent() {
406 std::fs::create_dir_all(parent)
407 .with_context(|| format!("Creating {}", parent.display()))?;
408 }
409 std::fs::write(&path, conf).with_context(|| format!("Writing {}", path.display()))
410}
411
412#[context("Running systemd-repart")]
415fn systemd_repart(
416 device: &Device,
417 root_size: Option<u64>,
418 rootfs: Option<Filesystem>,
419 generic_image: bool,
420) -> Result<PartitionLayout> {
421 let dry_partitions = systemd_repart_run(device, false, true, RepartDefinitions::Image)?;
425
426 if dry_partitions.is_empty() {
427 anyhow::bail!("systemd-repart returned empty partitions");
428 }
429
430 let has_root = dry_partitions
431 .iter()
432 .any(|p| p.partition_type.starts_with("root-"));
433
434 if has_root {
435 let filtered;
437 let definitions = if need_filtered_definitions(generic_image)? {
438 filtered =
439 tempfile::tempdir().context("Creating a directory for repart.d definitions")?;
440 collect_repart_definitions(
441 &dry_partitions,
442 filtered.path(),
443 repart_partition_is_required,
444 )?;
445 RepartDefinitions::Only(filtered.path())
446 } else {
447 RepartDefinitions::Image
448 };
449 let partitions = systemd_repart_run(device, generic_image, false, definitions)?;
450 let layout = parse_repart_layout(&partitions)?;
451 return Ok(layout);
452 }
453
454 let Some(fs) = rootfs else {
456 anyhow::bail!("Rootfs not specified");
457 };
458 let mut root_conf = format!("[Partition]\nType=root\nFormat={fs}\n");
459 if let Some(size_mib) = root_size {
460 writeln!(root_conf, "SizeMinBytes={size_mib}M")?;
461 writeln!(root_conf, "SizeMaxBytes={size_mib}M")?;
462 }
463 let generated = tempfile::tempdir().context("Creating a directory for repart.d definitions")?;
465 let root_name = generated_root_definition_name();
466
467 let boot_partition_sizes = if generic_image && root_size.is_none() {
475 write_repart_file(generated.path(), &root_name, &root_conf)?;
476 let planned = systemd_repart_run(
477 device,
478 false,
479 true,
480 RepartDefinitions::WithGenerated(generated.path()),
481 )
482 .context("Planning the root partition size")?;
483
484 let size_mib = planned_root_size_mib(&planned)?.get();
485 tracing::debug!(
486 "Root partition size: {size_mib} MiB of {} MiB",
487 device.size / MIB
488 );
489 writeln!(root_conf, "SizeMinBytes={size_mib}M")?;
490 writeln!(root_conf, "SizeMaxBytes={size_mib}M")?;
491
492 planned_boot_partition_sizes(&planned)?
493 } else {
494 Vec::new()
495 };
496
497 let definitions = if need_filtered_definitions(generic_image)? {
498 collect_repart_definitions(
500 &dry_partitions,
501 generated.path(),
502 repart_partition_is_required,
503 )?;
504 RepartDefinitions::Only(generated.path())
505 } else {
506 RepartDefinitions::WithGenerated(generated.path())
507 };
508 write_repart_file(generated.path(), &root_name, &root_conf)?;
509 for (name, size) in &boot_partition_sizes {
511 let dropin = format!("{name}.d/{REPART_PLANNED_SIZE_DROPIN}");
512 write_repart_file(generated.path(), &dropin, &planned_size_pin(*size))?;
513 }
514
515 let partitions = systemd_repart_run(device, generic_image, false, definitions)?;
516 let layout = parse_repart_layout(&partitions)?;
517
518 Ok(layout)
519}
520
521#[derive(Debug, Clone, Copy)]
523enum RepartDefinitions<'a> {
524 Image,
526 WithGenerated(&'a Path),
528 Only(&'a Path),
531}
532
533impl RepartDefinitions<'_> {
534 fn args(self, generic_image: bool) -> Vec<OsString> {
537 let definitions = |dir: &OsStr| {
538 let mut arg = OsString::from("--definitions=");
539 arg.push(dir);
540 arg
541 };
542 let mut args = Vec::new();
543 match self {
544 Self::Only(dir) => args.push(definitions(dir.as_os_str())),
547 Self::Image | Self::WithGenerated(_) => {
548 if let Self::WithGenerated(generated) = self {
549 args.extend(
550 REPART_CONFIG_DIRS
551 .iter()
552 .map(|dir| definitions(OsStr::new(dir))),
553 );
554 args.push(definitions(generated.as_os_str()));
555 }
556 if generic_image {
564 args.push(format!("--include-partitions=root,esp,{BIOS_BOOT}").into());
565 }
566 }
567 }
568 args
569 }
570}
571
572fn systemd_repart_run(
576 device: &Device,
577 generic_image: bool,
578 dry_run: bool,
579 definitions: RepartDefinitions,
580) -> Result<Vec<RepartPartition>> {
581 let mut cmd = Command::new("systemd-repart");
582
583 cmd.env("SYSTEMD_REPART_MKFS_OPTIONS_EXT4", "-O verity");
588
589 let dry_run_arg = if dry_run {
590 "--dry-run=yes"
591 } else {
592 "--dry-run=no"
593 };
594 cmd.args([
595 dry_run_arg,
596 "--no-pager",
597 "--json=pretty",
598 "--empty=force",
599 "--root=/",
600 ]);
601
602 cmd.args(definitions.args(generic_image));
603 cmd.arg(device.path());
604
605 let output = cmd.output().context("Failed to run systemd-repart")?;
606
607 if !output.status.success() {
608 let stderr = String::from_utf8_lossy(&output.stderr);
609 anyhow::bail!("systemd-repart failed: {stderr}");
610 }
611
612 if !dry_run {
613 tracing::debug!(
614 "Partitions ({}): {}",
615 if dry_run { "Dry Run" } else { "Actual" },
616 std::str::from_utf8(&output.stdout).unwrap_or("Failed to serialize output as UTF-8")
617 );
618 }
619
620 serde_json::from_slice(&output.stdout).with_context(|| {
621 format!(
622 "Failed to deserialize systemd-repart output: {}",
623 String::from_utf8_lossy(&output.stdout)
624 )
625 })
626}
627
628fn parse_repart_layout(partitions: &[RepartPartition]) -> Result<PartitionLayout> {
630 let mut esp_partno = None;
631 let mut boot_partno = None;
632 let mut rootpn = None;
633
634 for (idx, part) in partitions.iter().enumerate() {
635 let partno = part.partno.unwrap_or(idx as u32) + 1;
639
640 match part.partition_type.as_str() {
641 "esp" => esp_partno = Some(partno),
642 "xbootldr" => boot_partno = Some(partno),
643 t if t.starts_with("root-") => rootpn = Some(partno),
644 _ => {}
645 }
646 }
647
648 let rootpn =
649 rootpn.ok_or_else(|| anyhow::anyhow!("systemd-repart output missing root partition"))?;
650
651 Ok(PartitionLayout {
652 esp_partno,
653 boot_partno,
654 rootpn,
655 used_repart: true,
656 })
657}
658
659fn aboot_partition_labels(layout: AbootDiskLayout) -> &'static [&'static str] {
660 match layout {
661 AbootDiskLayout::Android => &["boot_a", "boot_b", "vbmeta_a", "vbmeta_b"],
662 AbootDiskLayout::Ukiboot => &["ukiboot_a", "ukiboot_b", "ukibootctl"],
663 }
664}
665
666fn aboot_partition_type(label: &str) -> &'static str {
667 match label {
668 "boot_a" | "boot_b" => ABOOT_BOOT_GUID,
669 "vbmeta_a" | "vbmeta_b" => ABOOT_VBMETA_GUID,
670 "ukiboot_a" | "ukiboot_b" => UKIBOOT_BOOT_GUID,
671 "ukibootctl" => UKIBOOT_CTL_GUID,
672 _ => unreachable!("unknown aboot partition label"),
673 }
674}
675
676fn validate_aboot_repart_partitions(
677 partitions: &[RepartPartition],
678 layout: AbootDiskLayout,
679) -> Result<()> {
680 let other_layout = match layout {
681 AbootDiskLayout::Android => AbootDiskLayout::Ukiboot,
682 AbootDiskLayout::Ukiboot => AbootDiskLayout::Android,
683 };
684 let labels = aboot_partition_labels(layout);
685 let other_labels = aboot_partition_labels(other_layout);
686 anyhow::ensure!(
687 !partitions
688 .iter()
689 .any(|p| other_labels.contains(&p.label.as_deref().unwrap_or(""))),
690 "repart.d contains partitions for a different aboot layout"
691 );
692 for label in labels {
693 let count = partitions
694 .iter()
695 .filter(|p| p.label.as_deref() == Some(*label))
696 .count();
697 anyhow::ensure!(
698 count <= 1,
699 "Duplicate {label} partition in aboot repart.d layout"
700 );
701 if let Some(partition) = partitions
702 .iter()
703 .find(|p| p.label.as_deref() == Some(*label))
704 {
705 anyhow::ensure!(
706 partition
707 .partition_type
708 .eq_ignore_ascii_case(aboot_partition_type(label)),
709 "aboot partition {label} has the wrong type GUID"
710 );
711 }
712 }
713 Ok(())
714}
715
716fn validate_repart_boot_layout(
717 partitions: &[RepartPartition],
718 layout: Option<AbootDiskLayout>,
719) -> Result<()> {
720 if let Some(layout) = layout {
721 return validate_aboot_repart_partitions(partitions, layout);
722 }
723 anyhow::ensure!(
724 !partitions
725 .iter()
726 .any(|p| [AbootDiskLayout::Android, AbootDiskLayout::Ukiboot]
727 .iter()
728 .any(|layout| aboot_partition_labels(*layout)
729 .contains(&p.label.as_deref().unwrap_or("")))),
730 "aboot repart.d layout requires a matching aboot boot artifact"
731 );
732 Ok(())
733}
734
735fn image_repart_partitions(
736 device: &Device,
737 layout: Option<AbootDiskLayout>,
738) -> Result<Vec<RepartPartition>> {
739 if !can_use_systemd_repart() {
740 return Ok(Vec::new());
741 }
742 let partitions = systemd_repart_run(device, false, true, RepartDefinitions::Image)?;
743 validate_repart_boot_layout(&partitions, layout)?;
744 Ok(partitions)
745}
746
747fn verify_image_aboot_filesystems(
748 device: &Device,
749 aboot_layout: AbootDiskLayout,
750 partition_layout: &PartitionLayout,
751) -> Result<()> {
752 let labels = aboot_partition_labels(aboot_layout);
753 for label in labels {
754 let partition = device
755 .children
756 .iter()
757 .flatten()
758 .find(|part| part.partlabel.as_deref() == Some(label))
759 .ok_or_else(|| anyhow::anyhow!("Missing {label} partition"))?;
760 anyhow::ensure!(
761 partition
762 .parttype
763 .as_deref()
764 .is_some_and(|guid| guid.eq_ignore_ascii_case(aboot_partition_type(label))),
765 "aboot partition {label} has the wrong type GUID"
766 );
767 anyhow::ensure!(
768 partition.fstype.is_none(),
769 "aboot partition {label} must not contain a filesystem"
770 );
771 }
772 if aboot_layout == AbootDiskLayout::Ukiboot {
773 let esp = device
774 .find_device_by_partno(partition_layout.esp_partno.context("Missing ukiboot ESP")?)?;
775 anyhow::ensure!(
776 esp.fstype.as_deref() == Some("vfat"),
777 "ukiboot ESP must be formatted as vfat"
778 );
779 }
780 Ok(())
781}
782
783fn aboot_repart_definitions(
784 layout: AbootDiskLayout,
785 root_size: Option<u64>,
786 rootfs: Option<Filesystem>,
787 partitions: &[RepartPartition],
788) -> Result<Vec<(String, String)>> {
789 let mut definitions = Vec::new();
790 if layout == AbootDiskLayout::Ukiboot && !partitions.iter().any(|p| p.partition_type == "esp") {
791 let esp = format!(
792 "[Partition]\nType=esp\nFormat=vfat\nSizeMinBytes={UKIBOOT_ESP_SIZE_MB}M\nSizeMaxBytes={UKIBOOT_ESP_SIZE_MB}M\n"
793 );
794 definitions.push(("05-efi.conf".into(), esp));
795 }
796 let mut add = |order: u32, name: &str, part_type: &str, size: u32, flags: Option<&str>| {
797 if partitions.iter().any(|p| p.label.as_deref() == Some(name)) {
798 return;
799 }
800 let mut config = format!(
801 "[Partition]\nType={part_type}\nLabel={name}\nSizeMinBytes={size}M\nSizeMaxBytes={size}M\n"
802 );
803 if let Some(flags) = flags {
804 config.push_str(&format!("Flags={flags}\n"));
805 }
806 definitions.push((format!("{order:02}-{name}.conf"), config));
807 };
808
809 match layout {
810 AbootDiskLayout::Android => {
811 add(
812 10,
813 "boot_a",
814 ABOOT_BOOT_GUID,
815 ABOOT_BOOT_SIZE_MB,
816 Some("0x3f000000000000"),
817 );
818 add(
819 11,
820 "boot_b",
821 ABOOT_BOOT_GUID,
822 ABOOT_BOOT_SIZE_MB,
823 Some("0x3d000000000000"),
824 );
825 for (order, name) in [(12, "vbmeta_a"), (13, "vbmeta_b")] {
826 add(order, name, ABOOT_VBMETA_GUID, ABOOT_VBMETA_SIZE_MB, None);
827 }
828 }
829 AbootDiskLayout::Ukiboot => {
830 for (order, name) in [(10, "ukiboot_a"), (11, "ukiboot_b")] {
831 add(order, name, UKIBOOT_BOOT_GUID, ABOOT_BOOT_SIZE_MB, None);
832 }
833 add(
834 12,
835 "ukibootctl",
836 UKIBOOT_CTL_GUID,
837 UKIBOOT_CTL_SIZE_MB,
838 None,
839 );
840 }
841 }
842
843 if partitions
844 .iter()
845 .any(|p| p.partition_type.starts_with("root-"))
846 {
847 return Ok(definitions);
848 }
849 let rootfs = rootfs.context(
850 "No root repart.d definition found; specify --filesystem or a root filesystem in the install config",
851 )?;
852 let mut root = format!("[Partition]\nType=root\nFormat={rootfs}\n");
853 if let Some(size) = root_size {
854 root.push_str(&format!("SizeMinBytes={size}M\nSizeMaxBytes={size}M\n"));
855 }
856 definitions.push(("50-root.conf".into(), root));
857 Ok(definitions)
858}
859
860#[context("Running systemd-repart for aboot")]
861fn systemd_repart_aboot(
862 device: &Device,
863 layout: AbootDiskLayout,
864 image_partitions: &[RepartPartition],
865 generated_definitions: &[(String, String)],
866 generic_image: bool,
867) -> Result<PartitionLayout> {
868 let definitions = tempfile::tempdir().context("Creating aboot repart definitions directory")?;
869 collect_repart_definitions(image_partitions, definitions.path(), |part| {
870 !generic_image
871 || repart_partition_is_required(part)
872 || aboot_partition_labels(layout).contains(&part.label.as_deref().unwrap_or(""))
873 })?;
874 for (name, config) in generated_definitions {
875 std::fs::OpenOptions::new()
876 .write(true)
877 .create_new(true)
878 .open(definitions.path().join(name))
879 .with_context(|| format!("Creating generated repart definition {name}"))?
880 .write_all(config.as_bytes())?;
881 }
882 let partitions = systemd_repart_run(
883 device,
884 false,
885 false,
886 RepartDefinitions::Only(definitions.path()),
887 )?;
888 parse_repart_layout(&partitions)
889}
890
891#[context("Running sfdisk")]
893fn sfdisk(
894 device: &Device,
895 root_size: Option<u64>,
896 composefs_backend: bool,
897 requires_bootpart: bool,
898) -> Result<PartitionLayout> {
899 let mut partno = 0;
901 let mut partitioning_buf = String::new();
902 writeln!(partitioning_buf, "label: gpt")?;
903 let random_label = uuid::Uuid::new_v4();
904 writeln!(&mut partitioning_buf, "label-id: {random_label}")?;
905 if cfg!(target_arch = "x86_64") {
906 partno += 1;
907 writeln!(
908 &mut partitioning_buf,
909 r#"size=1MiB, bootable, type=21686148-6449-6E6F-744E-656564454649, name="BIOS-BOOT""#
910 )?;
911 } else if cfg!(target_arch = "powerpc64") {
912 partno += 1;
914 let label = PREPBOOT_LABEL;
915 let uuid = PREPBOOT_GUID;
916 writeln!(
917 &mut partitioning_buf,
918 r#"size=4MiB, bootable, type={uuid}, name="{label}""#
919 )?;
920 } else if cfg!(any(target_arch = "aarch64", target_arch = "s390x")) {
921 } else {
923 anyhow::bail!("Unsupported architecture: {}", std::env::consts::ARCH);
924 }
925
926 let esp_partno = if super::ARCH_USES_EFI {
927 let esp_guid = bootc_blockdev::dps::ESP;
928 partno += 1;
929
930 let esp_size = if composefs_backend {
931 CFS_EFIPN_SIZE_MB
932 } else {
933 EFIPN_SIZE_MB
934 };
935
936 writeln!(
937 &mut partitioning_buf,
938 r#"size={esp_size}MiB, type={esp_guid}, name="EFI-SYSTEM""#
939 )?;
940 Some(partno)
941 } else {
942 None
943 };
944
945 let boot_partno = if requires_bootpart {
949 partno += 1;
950 writeln!(
951 &mut partitioning_buf,
952 r#"size={BOOTPN_SIZE_MB}MiB, name="boot""#
953 )?;
954 Some(partno)
955 } else {
956 None
957 };
958 let rootpn = partno + 1;
959 let root_size = root_size
960 .map(|v| Cow::Owned(format!("size={v}MiB, ")))
961 .unwrap_or_else(|| Cow::Borrowed(""));
962 let rootpart_uuid = uuid::Uuid::parse_str(bootc_blockdev::dps::this_arch_root())?;
963 writeln!(
964 &mut partitioning_buf,
965 r#"{root_size}type={rootpart_uuid}, name="root""#
966 )?;
967 tracing::debug!("Partitioning: {partitioning_buf}");
968 Task::new("Initializing partitions", "sfdisk")
969 .arg("--wipe=always")
970 .arg(device.path())
971 .quiet()
972 .run_with_stdin_buf(Some(partitioning_buf.as_bytes()))
973 .context("Failed to run sfdisk")?;
974
975 Ok(PartitionLayout {
976 esp_partno,
977 boot_partno,
978 rootpn,
979 used_repart: false,
980 })
981}
982
983#[context("Creating rootfs")]
984#[cfg(feature = "install-to-disk")]
985pub(crate) fn install_create_rootfs(
986 state: &State,
987 opts: InstallBlockDeviceOpts,
988 run_repart: bool,
989) -> Result<RootSetup> {
990 let install_config = state.install_config.as_ref();
991 let luks_name = "root";
992 let mut device = bootc_blockdev::list_dev(&opts.device)?;
995
996 if is_mounted_in_pid1_mountns(&device.path())? {
998 anyhow::bail!("Device {} is mounted", device.path())
999 }
1000
1001 if opts.wipe {
1003 let dev = &opts.device;
1004 for child in device.children.iter().flatten() {
1005 let child = child.path();
1006 println!("Wiping {child}");
1007 wipefs(Utf8Path::new(&child))?;
1008 }
1009 println!("Wiping {dev}");
1010 wipefs(dev)?;
1011 } else if device.has_children() {
1012 anyhow::bail!(
1013 "Detected existing partitions on {}; use e.g. `wipefs` or --wipe if you intend to overwrite",
1014 opts.device
1015 );
1016 }
1017
1018 let run_bootc = Utf8Path::new(RUN_BOOTC);
1019 let mntdir = run_bootc.join("mounts");
1020 if mntdir.exists() {
1021 std::fs::remove_dir_all(&mntdir)?;
1022 }
1023
1024 if state.aboot_disk_layout.is_some() {
1025 anyhow::ensure!(
1026 has_systemd_repart(),
1027 "aboot to-disk install requires systemd-repart"
1028 );
1029 }
1030 let image_partitions = if state.aboot_disk_layout.is_some() || run_repart {
1031 image_repart_partitions(&device, state.aboot_disk_layout)?
1032 } else {
1033 Vec::new()
1034 };
1035 let use_systemd_repart =
1036 state.aboot_disk_layout.is_some() || (run_repart && can_use_systemd_repart());
1037
1038 let block_setup = if let Some(config) = install_config {
1040 config.get_block_setup(opts.block_setup.as_ref().copied())?
1041 } else if opts.filesystem.is_some() || use_systemd_repart {
1042 opts.block_setup.unwrap_or_default()
1045 } else {
1046 anyhow::bail!("No install configuration found, and no filesystem specified")
1049 };
1050 let serial = device.serial.as_deref().unwrap_or("<unknown>");
1051 let model = device.model.as_deref().unwrap_or("<unknown>");
1052 let discoverable = state.aboot_disk_layout.is_none() && use_discoverable_partitions(state);
1053 if let Some(layout) = state.aboot_disk_layout {
1054 anyhow::ensure!(
1055 block_setup == BlockSetup::Direct,
1056 "{layout:?} aboot layout requires --block-setup direct"
1057 );
1058 anyhow::ensure!(
1059 layout != AbootDiskLayout::Ukiboot || super::ARCH_USES_EFI,
1060 "ukiboot disk layout requires x86_64 or aarch64"
1061 );
1062 anyhow::ensure!(
1063 state.composefs_options.composefs_backend,
1064 "aboot disk layouts require the composefs backend"
1065 );
1066 }
1067 println!("Block setup: {block_setup}");
1068 println!(" Size: {}", device.size);
1069 println!(" Serial: {serial}");
1070 println!(" Model: {model}");
1071 println!(
1072 " Partitions: {}",
1073 if discoverable { "Discoverable" } else { "UUID" }
1074 );
1075
1076 let root_size = opts
1077 .root_size
1078 .as_deref()
1079 .map(bootc_blockdev::parse_size_mib)
1080 .transpose()
1081 .context("Parsing root size")?;
1082 let aboot_definitions = state
1083 .aboot_disk_layout
1084 .map(|layout| {
1085 aboot_repart_definitions(
1086 layout,
1087 root_size,
1088 opts.filesystem.or(install_config
1089 .and_then(|c| c.filesystem_root())
1090 .and_then(|r| r.fstype)),
1091 &image_partitions,
1092 )
1093 })
1094 .transpose()?;
1095
1096 let sepolicy = state.load_policy()?;
1098 let sepolicy = sepolicy.as_ref();
1099
1100 let physical_root_path = mntdir.join("rootfs");
1103 std::fs::create_dir_all(&physical_root_path)?;
1104 let bootfs = mntdir.join("boot");
1105 std::fs::create_dir_all(bootfs)?;
1106
1107 let layout = if let Some(aboot_disk_layout) = state.aboot_disk_layout {
1108 systemd_repart_aboot(
1109 &device,
1110 aboot_disk_layout,
1111 &image_partitions,
1112 aboot_definitions.as_deref().unwrap(),
1113 state.config_opts.generic_image,
1114 )?
1115 } else if use_systemd_repart {
1116 systemd_repart(
1117 &device,
1118 root_size,
1119 opts.filesystem,
1120 state.config_opts.generic_image,
1121 )?
1122 } else {
1123 sfdisk(
1124 &device,
1125 root_size,
1126 state.composefs_options.composefs_backend,
1127 block_setup.requires_bootpart(),
1128 )?
1129 };
1130
1131 tracing::debug!(
1132 "Created partition table using {}",
1133 if layout.used_repart {
1134 "systemd-repart"
1135 } else {
1136 "sfdisk"
1137 }
1138 );
1139
1140 udev_settle()?;
1143
1144 device.refresh()?;
1146
1147 if let Some(aboot_layout) = state.aboot_disk_layout {
1148 verify_image_aboot_filesystems(&device, aboot_layout, &layout)?;
1149 }
1150
1151 let root_filesystem = if layout.used_repart {
1153 let root = device.find_device_by_partno(layout.rootpn)?;
1154 root.fstype
1155 .as_ref()
1156 .ok_or_else(|| anyhow::anyhow!("repart: Root filesystem type not defined"))?
1157 } else {
1158 let root_filesystem = opts
1159 .filesystem
1160 .or(install_config
1161 .and_then(|c| c.filesystem_root())
1162 .and_then(|r| r.fstype))
1163 .ok_or_else(|| anyhow::anyhow!("No root filesystem specified"))?;
1164
1165 &root_filesystem.to_string()
1166 };
1167
1168 let root_device = device.find_device_by_partno(layout.rootpn)?;
1169
1170 let expected_parttype = bootc_blockdev::dps::this_arch_root();
1172 if !root_device
1173 .parttype
1174 .as_ref()
1175 .is_some_and(|pt| pt.eq_ignore_ascii_case(expected_parttype))
1176 {
1177 anyhow::bail!(
1178 "root partition {} has type {}; expected {expected_parttype}",
1179 layout.rootpn,
1180 root_device.parttype.as_deref().unwrap_or("<none>")
1181 );
1182 }
1183 let (rootdev_path, root_blockdev_kargs) = match block_setup {
1184 BlockSetup::Direct => (root_device.path(), None),
1185 BlockSetup::Tpm2Luks => {
1186 let uuid = uuid::Uuid::new_v4().to_string();
1187 let dummy_passphrase = uuid::Uuid::new_v4().to_string();
1189 let mut tmp_keyfile = tempfile::NamedTempFile::new()?;
1190 tmp_keyfile.write_all(dummy_passphrase.as_bytes())?;
1191 tmp_keyfile.flush()?;
1192 let tmp_keyfile = tmp_keyfile.path();
1193 let dummy_passphrase_input = Some(dummy_passphrase.as_bytes());
1194
1195 let root_devpath = root_device.path();
1196
1197 Task::new("Initializing LUKS for root", "cryptsetup")
1198 .args(["luksFormat", "--uuid", uuid.as_str(), "--key-file"])
1199 .args([tmp_keyfile])
1200 .arg(&root_devpath)
1201 .run()?;
1202 Task::new("Enrolling root device with TPM", "systemd-cryptenroll")
1205 .args(["--wipe-slot=all", "--tpm2-device=auto", "--unlock-key-file"])
1206 .args([tmp_keyfile])
1207 .arg(&root_devpath)
1208 .verbose()
1209 .run_with_stdin_buf(dummy_passphrase_input)?;
1210 Task::new("Opening root LUKS device", "cryptsetup")
1211 .args(["luksOpen", &root_devpath, luks_name])
1212 .run()?;
1213 let rootdev = format!("/dev/mapper/{luks_name}");
1214 let kargs = vec![
1215 format!("luks.uuid={uuid}"),
1216 format!("luks.options=tpm2-device=auto,headless=true"),
1217 ];
1218 (rootdev, Some(kargs))
1219 }
1220 };
1221
1222 let bootdev = if let Some(bootpn) = layout.boot_partno {
1224 Some(device.find_device_by_partno(bootpn)?)
1225 } else {
1226 None
1227 };
1228
1229 let boot_uuid = match bootdev {
1230 Some(bootdev) => {
1231 let u = if layout.used_repart {
1232 let u = bootdev
1233 .uuid
1234 .as_ref()
1235 .ok_or_else(|| anyhow::anyhow!("bootdev UUID not found"))?;
1236
1237 u.parse::<uuid::Uuid>()
1238 .with_context(|| format!("Parsing bootdev UUID {u}"))?
1239 } else {
1240 mkfs(
1241 &bootdev.path(),
1242 root_filesystem.as_str().try_into()?,
1243 "boot",
1244 opts.wipe,
1245 [],
1246 )
1247 .context("Initializing /boot")?
1248 };
1249
1250 Some(u)
1251 }
1252 None => None,
1253 };
1254
1255 let root_uuid = if layout.used_repart {
1256 let u = root_device.uuid.as_ref().ok_or_else(|| {
1258 anyhow::anyhow!("Root device created by repart has no filesystem UUID")
1259 })?;
1260
1261 if systemd_version()? < REPART_MKFS_OPTIONS_MIN_VERSION && root_filesystem == "ext4" {
1264 tracing::debug!("Manually enabling fs-verity on root partition");
1265
1266 Command::new("tune2fs")
1267 .args(["-O", "verity", root_device.path().as_str()])
1268 .run_inherited()
1269 .context("Running tune2fs enabling fs-verity")?;
1270 }
1271
1272 u.parse::<uuid::Uuid>()
1273 .with_context(|| format!("Parsing root fs UUID {u}"))?
1274 } else {
1275 let rootfs: Filesystem = root_filesystem.as_str().try_into()?;
1276
1277 let mkfs_options = match rootfs {
1279 Filesystem::Ext4 => ["-O", "verity"].as_slice(),
1280 _ => [].as_slice(),
1281 };
1282
1283 mkfs(
1284 &rootdev_path,
1285 rootfs,
1286 "root",
1287 opts.wipe,
1288 mkfs_options.iter().copied(),
1289 )?
1290 };
1291 let bootsrc = boot_uuid.as_ref().map(|uuid| format!("UUID={uuid}"));
1292 let bootarg = bootsrc.as_deref().map(|bootsrc| format!("boot={bootsrc}"));
1293 let boot = bootsrc.map(|bootsrc| MountSpec {
1294 source: bootsrc,
1295 target: format!("/{BOOT}"),
1296 fstype: MountSpec::AUTO.into(),
1297 options: Some("ro".into()),
1298 });
1299
1300 let mut kargs = Cmdline::new();
1301
1302 if let Some(root_blockdev_kargs) = root_blockdev_kargs {
1304 for karg in root_blockdev_kargs {
1305 kargs.extend(&Cmdline::from(karg.as_str()));
1306 }
1307 }
1308
1309 if discoverable {
1312 kargs.extend(&Cmdline::from(RW_KARG));
1313 } else {
1314 let rootarg = format!("root=UUID={root_uuid}");
1315 kargs.extend(&Cmdline::from(format!("{rootarg} {RW_KARG}")));
1316 }
1317
1318 if let Some(bootarg) = bootarg {
1320 kargs.extend(&Cmdline::from(bootarg.as_str()));
1321 }
1322
1323 if let Some(cli_kargs) = state.config_opts.karg.as_ref() {
1325 for karg in cli_kargs {
1326 kargs.extend(karg);
1327 }
1328 }
1329
1330 let fstype = &root_filesystem.to_string();
1331 bootc_mount::mount_typed(&rootdev_path, fstype, &physical_root_path)?;
1332 let target_rootfs = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
1333 crate::lsm::ensure_dir_labeled(&target_rootfs, "", Some("/".into()), 0o755.into(), sepolicy)?;
1334 let physical_root = Dir::open_ambient_dir(&physical_root_path, cap_std::ambient_authority())?;
1335 let bootfs = physical_root_path.join(BOOT);
1336 crate::lsm::ensure_dir_labeled(&target_rootfs, BOOT, None, 0o755.into(), sepolicy)?;
1338 if let Some(bootdev) = bootdev {
1339 bootc_mount::mount_typed(&bootdev.path(), fstype, &bootfs)?;
1340 }
1341 crate::lsm::ensure_dir_labeled(&target_rootfs, BOOT, None, 0o755.into(), sepolicy)?;
1343
1344 if let Some(esp_partno) = layout.esp_partno {
1346 let espdev = device.find_device_by_partno(esp_partno)?;
1347 if !layout.used_repart {
1348 Task::new("Creating ESP filesystem", "mkfs.fat")
1349 .args([&espdev.path(), "-n", "EFI-SYSTEM"])
1350 .verbose()
1351 .quiet_output()
1352 .run()?;
1353 }
1354 let efifs_path = bootfs.join(crate::bootloader::EFI_DIR);
1355 std::fs::create_dir(&efifs_path).context("Creating efi dir")?;
1356 }
1357
1358 let luks_device = match block_setup {
1359 BlockSetup::Direct => None,
1360 BlockSetup::Tpm2Luks => Some(luks_name.to_string()),
1361 };
1362 Ok(RootSetup {
1363 luks_device,
1364 device_info: device,
1365 physical_root_path,
1366 physical_root,
1367 target_root_path: None,
1368 rootfs_uuid: Some(root_uuid.to_string()),
1369 aboot_disk_layout: state.aboot_disk_layout,
1370 disk_install: true,
1371 boot,
1372 kargs,
1373 skip_finalize: false,
1374 })
1375}
1376
1377#[cfg(test)]
1378mod tests {
1379 use super::*;
1380
1381 #[test]
1382 fn aboot_repart_specs() {
1383 let android =
1384 aboot_repart_definitions(AbootDiskLayout::Android, None, Some(Filesystem::Ext4), &[])
1385 .unwrap();
1386 assert_eq!(android.len(), 5);
1387 assert_eq!(android[0].0, "10-boot_a.conf");
1388 assert!(android[0].1.contains("Flags=0x3f000000000000\n"));
1389 assert!(android[1].1.contains("Flags=0x3d000000000000\n"));
1390 assert!(android[4].1.contains("Type=root\nFormat=ext4\n"));
1391
1392 let ukiboot = aboot_repart_definitions(
1393 AbootDiskLayout::Ukiboot,
1394 Some(4096),
1395 Some(Filesystem::Ext4),
1396 &[],
1397 )
1398 .unwrap();
1399 assert_eq!(ukiboot.len(), 5);
1400 assert_eq!(ukiboot[0].0, "05-efi.conf");
1401 assert!(ukiboot[0].1.contains("Type=esp\nFormat=vfat\n"));
1402 assert!(
1403 ukiboot[4]
1404 .1
1405 .contains("SizeMinBytes=4096M\nSizeMaxBytes=4096M\n")
1406 );
1407 }
1408
1409 #[test]
1410 fn image_aboot_repart_layout() -> Result<()> {
1411 let mut partitions: Vec<RepartPartition> = serde_json::from_value(serde_json::json!([
1412 { "type": "20117f86-e985-4357-b9ee-374bc1d8487d", "label": "boot_a", "file": "10-boot-a.conf" },
1413 { "type": "20117f86-e985-4357-b9ee-374bc1d8487d", "label": "boot_b", "file": "11-boot-b.conf" },
1414 { "type": "4b7a15d6-322c-42ac-8110-88b7da0c5d77", "label": "vbmeta_a", "file": "12-vbmeta-a.conf" },
1415 { "type": "4b7a15d6-322c-42ac-8110-88b7da0c5d77", "label": "vbmeta_b", "file": "13-vbmeta-b.conf" },
1416 { "type": "root-arm64", "label": "root", "file": "50-root.conf" }
1417 ]))?;
1418 validate_aboot_repart_partitions(&partitions, AbootDiskLayout::Android)?;
1419 assert!(validate_repart_boot_layout(&partitions, None).is_err());
1420 assert!(validate_repart_boot_layout(&partitions, Some(AbootDiskLayout::Ukiboot)).is_err());
1421 validate_repart_boot_layout(&[], None)?;
1422 let correct_type =
1423 std::mem::replace(&mut partitions[0].partition_type, "linux-generic".into());
1424 assert!(validate_aboot_repart_partitions(&partitions, AbootDiskLayout::Android).is_err());
1425 partitions[0].partition_type = correct_type;
1426
1427 let unrelated: RepartPartition = serde_json::from_value(serde_json::json!({
1428 "type": ABOOT_BOOT_GUID,
1429 "label": "cache",
1430 "file": "20-cache.conf"
1431 }))?;
1432 assert!(!repart_partition_is_required(&partitions[0]));
1433 assert!(!repart_partition_is_required(&unrelated));
1434 validate_aboot_repart_partitions(&[], AbootDiskLayout::Android)?;
1435 validate_aboot_repart_partitions(&partitions[..3], AbootDiskLayout::Android)?;
1436 let missing =
1437 aboot_repart_definitions(AbootDiskLayout::Android, None, None, &partitions[1..])?;
1438 assert_eq!(missing.len(), 1);
1439 assert_eq!(missing[0].0, "10-boot_a.conf");
1440 Ok(())
1441 }
1442
1443 #[test]
1444 fn aboot_preserves_image_repart_definitions() -> Result<()> {
1445 for (layout, generic_image) in [
1446 (AbootDiskLayout::Android, false),
1447 (AbootDiskLayout::Android, true),
1448 (AbootDiskLayout::Ukiboot, false),
1449 (AbootDiskLayout::Ukiboot, true),
1450 ] {
1451 let source = tempfile::tempdir()?;
1452 let destination = tempfile::tempdir()?;
1453 let root_config = "[Partition]\nType=root\nFormat=ext4\nLabel=custom-root\nSizeMinBytes=2G\nSizeMaxBytes=4G\n";
1454 let root_file = source.path().join("20-root.conf");
1455 std::fs::write(&root_file, root_config)?;
1456 let var_file = source.path().join("30-var.conf");
1457 std::fs::write(&var_file, "[Partition]\nType=var\nFormat=ext4\n")?;
1458 let mut partitions: Vec<RepartPartition> = serde_json::from_value(serde_json::json!([
1459 { "type": "root-x86-64", "label": "custom-root", "file": root_file },
1460 { "type": "var", "file": var_file }
1461 ]))?;
1462 let esp_config =
1463 "[Partition]\nType=esp\nFormat=vfat\nLabel=custom-esp\nSizeMinBytes=512M\n";
1464 if layout == AbootDiskLayout::Ukiboot {
1465 let esp_file = source.path().join("05-esp.conf");
1466 std::fs::write(&esp_file, esp_config)?;
1467 partitions.push(serde_json::from_value(serde_json::json!({
1468 "type": "esp", "file": esp_file
1469 }))?);
1470 }
1471
1472 validate_aboot_repart_partitions(&partitions, layout)?;
1473 let generated = aboot_repart_definitions(layout, Some(8192), None, &partitions)?;
1474 assert_eq!(generated.len(), aboot_partition_labels(layout).len());
1475 assert!(
1476 generated
1477 .iter()
1478 .all(|(_, config)| !config.contains("Format="))
1479 );
1480 collect_repart_definitions(&partitions, destination.path(), |part| {
1481 !generic_image
1482 || repart_partition_is_required(part)
1483 || aboot_partition_labels(layout).contains(&part.label.as_deref().unwrap_or(""))
1484 })?;
1485 assert_eq!(
1486 std::fs::read_to_string(destination.path().join("20-root.conf"))?,
1487 root_config
1488 );
1489 assert_eq!(
1490 destination.path().join("30-var.conf").exists(),
1491 !generic_image
1492 );
1493 if layout == AbootDiskLayout::Ukiboot {
1494 assert_eq!(
1495 std::fs::read_to_string(destination.path().join("05-esp.conf"))?,
1496 esp_config
1497 );
1498 }
1499 assert!(aboot_repart_definitions(layout, None, None, &[]).is_err());
1500 }
1501 Ok(())
1502 }
1503
1504 #[test]
1505 fn test_planned_root_size_mib() {
1506 use std::result::Result::Ok;
1508 let cases = [
1509 (
1514 include_str!("../fixtures/repart-dry-run-v262.json"),
1515 Ok(7166),
1516 ),
1517 (
1520 r#"[{"type": "swap", "file": "30-swap.conf", "raw_size": 1073741824, "raw_padding": 7515123712}]"#,
1521 Err("planned no root partition"),
1522 ),
1523 (
1524 r#"[{"type": "root-x86-64", "file": "50-root.conf", "raw_size": 1044480}]"#,
1525 Err("less than 1 MiB"),
1526 ),
1527 (
1528 r#"[{"type": "root-x86-64", "file": "50-root.conf"}]"#,
1529 Err("less than 1 MiB"),
1530 ),
1531 ];
1532 for (json, expected) in cases {
1533 let planned: Vec<RepartPartition> = serde_json::from_str(json).unwrap();
1534 match (planned_root_size_mib(&planned), expected) {
1535 (Ok(size_mib), Ok(expected)) => assert_eq!(size_mib.get(), expected),
1536 (Err(e), Err(expected)) => {
1537 assert!(format!("{e:#}").contains(expected), "{e:#}")
1538 }
1539 (result, expected) => panic!("{json}: got {result:?}, expected {expected:?}"),
1540 }
1541 }
1542 }
1543
1544 #[test]
1545 fn test_planned_boot_partition_sizes() {
1546 let cases = [
1547 (
1550 include_str!("../fixtures/repart-dry-run-v262.json"),
1551 vec![("00-esp.conf", 1073741824)],
1552 ),
1553 (
1555 r#"[
1556 {"type": "21686148-6449-6e6f-744e-656564454649", "file": "00-bios.conf", "raw_size": 1048576},
1557 {"type": "esp", "file": "00-esp.conf", "raw_size": 536870912},
1558 {"type": "xbootldr", "file": "10-xbootldr.conf", "raw_size": 1073741824},
1559 {"type": "root-x86-64", "file": "50-root.conf", "raw_size": 7516192768}
1560 ]"#,
1561 vec![("00-bios.conf", 1048576), ("00-esp.conf", 536870912)],
1562 ),
1563 (r#"[]"#, vec![]),
1564 ];
1565 for (json, expected) in cases {
1566 let planned: Vec<RepartPartition> = serde_json::from_str(json).unwrap();
1567 let sizes = planned_boot_partition_sizes(&planned).unwrap();
1568 let sizes = sizes
1569 .iter()
1570 .map(|(name, size)| (name.as_str(), *size))
1571 .collect::<Vec<_>>();
1572 assert_eq!(sizes, expected, "{json}");
1573 }
1574 }
1575
1576 #[test]
1577 fn test_repart_definitions_args() {
1578 use RepartDefinitions::{Image, Only, WithGenerated};
1579 let include = format!("--include-partitions=root,esp,{BIOS_BOOT}");
1580 let with_generated = [
1581 "--definitions=/etc/repart.d",
1582 "--definitions=/run/repart.d",
1583 "--definitions=/usr/local/lib/repart.d",
1584 "--definitions=/usr/lib/repart.d",
1585 "--definitions=/tmp/generated",
1586 ];
1587 let generated = WithGenerated(Path::new("/tmp/generated"));
1588 let copies = Only(Path::new("/tmp/copies"));
1589 let cases = [
1590 (Image, false, vec![]),
1591 (Image, true, vec![include.as_str()]),
1592 (generated, false, with_generated.to_vec()),
1593 (
1594 generated,
1595 true,
1596 [&with_generated[..], &[include.as_str()]].concat(),
1597 ),
1598 (copies, false, vec!["--definitions=/tmp/copies"]),
1600 (copies, true, vec!["--definitions=/tmp/copies"]),
1601 ];
1602 for (definitions, generic_image, expected) in cases {
1603 let expected = expected.into_iter().map(OsString::from).collect::<Vec<_>>();
1604 assert_eq!(
1605 definitions.args(generic_image),
1606 expected,
1607 "{definitions:?}, generic image: {generic_image}"
1608 );
1609 }
1610 }
1611}