diff --git a/Cargo.lock b/Cargo.lock index 71f7905..0da6e4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -202,6 +202,15 @@ version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "bytesize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" +dependencies = [ + "serde", +] + [[package]] name = "cc" version = "1.0.83" @@ -750,6 +759,7 @@ name = "katsu" version = "0.1.0" dependencies = [ "block-utils", + "bytesize", "clap 4.4.6", "cmd_lib", "color-eyre", diff --git a/Cargo.toml b/Cargo.toml index 573872c..3fdb2aa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,3 +40,4 @@ clap = { version = "4.4", features = ["derive", "env"] } nix = { version = "0.27", features = ["mount", "hostname", "dir"] } uuid = "1.4.1" loopdev = { git = "https://github.com/mdaffin/loopdev.git", version = "0.5.0" } +bytesize = { version = "1.3.0", features = ["serde"] } diff --git a/src/builder.rs b/src/builder.rs index 70da2f3..a5ebac3 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,19 +1,14 @@ use color_eyre::Result; -use gpt::{ - disk::{self, DEFAULT_SECTOR_SIZE}, - partition_types::{Type, EFI, LINUX_FS}, -}; use serde_derive::{Deserialize, Serialize}; use std::{ collections::BTreeMap, fs, io::{Seek, Write}, - path::{Path, PathBuf}, + path::PathBuf, }; -use tracing::{debug, info, warn}; +use tracing::{debug, info, warn, trace}; use crate::{ - chroot_run_cmd, cli::OutputFormat, config::{Manifest, Script}, util, @@ -72,6 +67,18 @@ impl RootBuilder for DnfRootBuilder { run_scripts(manifest.scripts.pre.clone(), &chroot, false)?; + if let Some(disk) = manifest.clone().disk { + let f = disk.fstab(&chroot)?; + trace!(fstab = ?f, "fstab"); + // write fstab to chroot + std::fs::create_dir_all(chroot.join("etc"))?; + let fstab_path = chroot.join("etc/fstab"); + let mut fstab_file = fs::File::create(fstab_path)?; + fstab_file.write_all(f.as_bytes())?; + fstab_file.flush()?; + drop(fstab_file); + } + let mut packages = self.packages.clone(); let mut options = self.options.clone(); let exclude = self.exclude.clone(); @@ -183,185 +190,53 @@ impl ImageBuilder for DiskImageBuilder { // create sparse file on disk let sparse_path = &image.canonicalize()?.join("katsu.img"); debug!(image = ?sparse_path, "Creating sparse file"); - let mut sparse_file = fs::File::create(sparse_path)?; - - // allocate 8GB (hardcoded for now) - // todo: unhardcode - // sparse_file.set_len(8 * 1024 * 1024 * 1024)?; - sparse_file.seek(std::io::SeekFrom::Start(8 * 1024 * 1024 * 1024))?; - sparse_file.write_all(&[0])?; - - // sparse_file.flush()?; - // drop(sparse_file); - /* - // use gpt crate to create gpt table - let mbr = gpt::mbr::ProtectiveMBR::with_lb_size( - u32::try_from((2 * 1024 * 1024) / 512).unwrap_or(0xFF_FF_FF_FF), - ); - mbr.overwrite_lba0(&mut sparse_file)?; - let header = - gpt::header::read_header_from_arbitrary_device(&mut sparse_file, DEFAULT_SECTOR_SIZE)?; - - let h = header.write_primary(&mut sparse_file, DEFAULT_SECTOR_SIZE)?; - - let mut disk = gpt::GptConfig::new() - .writable(true) - .initialized(false) - .logical_block_size(disk::LogicalBlockSize::Lb512) - .create_from_device(Box::new(sparse_file), None)?; - - // Create partition table - // let disk = gpt_cfg.open_from_device(Box::new(sparse_file))?; - // let mut disk = gpt_cfg.create_from_device(Box::new(sparse_file), None)?; - - debug!(disk = ?disk, "Disk"); - - disk.write_inplace()?; - - // create EFI partition (250mb) - let mut efi_partition = disk.add_partition( - "EFI", - 250 * 1024 * 1024, - EFI, - gpt::partition::PartitionAttributes::all().bits(), - None, - )?; - disk.write_inplace()?; - // create root partition (rest of disk) + // Error checking - // get the remaining size of the disk - let free_sectors = disk.find_free_sectors(); - debug!(free_sectors = ?free_sectors, "Free sectors"); - - let mut root_partition = disk.add_partition( - "ROOT", - 4 * 1024 * 1024 * 1024, - LINUX_FS, - gpt::partition::PartitionAttributes::empty().bits(), - None, - )?; - - disk.write_inplace()?; - - // disk. */ - - // todo: make the above code work - // for now, we'll just use fdisk and a shell script - // let sparse_path_str = sparse_path.to_str().unwrap(); + if manifest.clone().disk.is_none() { + // error out + return Err(color_eyre::eyre::eyre!("Disk layout not specified")); + } else { + info!("Disk layout specified"); + if manifest.clone().disk.unwrap().size.is_none() { + return Err(color_eyre::eyre::eyre!("Disk size not specified")); + } + } - // parted with heredoc + let mut sparse_file = fs::File::create(sparse_path)?; - cmd_lib::run_cmd!( - parted -s ${sparse_path} mklabel gpt; - parted -s ${sparse_path} mkpart primary fat32 1MiB 250MiB; - parted -s ${sparse_path} set 1 esp on; - parted -s ${sparse_path} name 1 EFI; - parted -s ${sparse_path} mkpart primary ext4 250MiB 1.25GiB; - parted -s ${sparse_path} name 2 BOOT; - parted -s ${sparse_path} mkpart primary ext4 1.25GiB 100%; - parted -s ${sparse_path} name 3 ROOT; - parted -s ${sparse_path} print; - )?; + let disk_size = manifest.clone().disk.unwrap().size.unwrap(); - // now mount them as loop devices + sparse_file.seek(std::io::SeekFrom::Start(disk_size.as_u64()))?; + sparse_file.write_all(&[0])?; + // let's mount the disk as a loop device let lc = loopdev::LoopControl::open()?; let loopdev = lc.next_free()?; - loopdev.attach_file(&sparse_path)?; - - // scan partitions - let loopdev_path = loopdev.path().unwrap(); - cmd_lib::run_cmd!( - partprobe ${loopdev_path}; - )?; - - // Format partitions - // todo: unhardcode - cmd_lib::run_cmd!( - mkfs.vfat -F 32 ${loopdev_path}p1; - mkfs.ext4 ${loopdev_path}p2; - mkfs.ext4 ${loopdev_path}p3; - )?; - - // mount partitions using nix mount - - let efi_loopdev = PathBuf::from(format!("{}p1", loopdev_path.display())); - let boot_loopdev = PathBuf::from(format!("{}p2", loopdev_path.display())); - let root_loopdev = PathBuf::from(format!("{}p3", loopdev_path.display())); - - let chroot = chroot.canonicalize()?; - - debug!( - efi_loopdev = ?efi_loopdev, - boot_loopdev = ?boot_loopdev, - root_loopdev = ?root_loopdev, - chroot = ?chroot, - ); - - let mount_table: [(&PathBuf, &PathBuf, &str); 3] = [ - (&root_loopdev, &chroot, "ext4"), - (&boot_loopdev, &chroot.join("boot"), "ext4"), - (&efi_loopdev, &chroot.join("boot/efi"), "vfat"), - ]; - - // mkdir - fs::create_dir_all(&chroot)?; - - for (ld, path, fs) in &mount_table { - debug!(ld = ?ld, path = ?path, fs = ?fs, "Mounting Device"); - fs::create_dir_all(*path)?; - nix::mount::mount( - Some(*ld), - *path, - Some(*fs), - nix::mount::MsFlags::empty(), - None::<&str>, - )?; - } - - /* nix::mount::mount( - Some(&root_loopdev), - &chroot, - Some("btrfs"), - nix::mount::MsFlags::empty(), - None::<&str>, - )?; - - fs::create_dir_all(&chroot.join("boot"))?; + loopdev.attach_file(sparse_path)?; - nix::mount::mount( - Some(&boot_loopdev), - &chroot.join("boot"), - Some("ext4"), - nix::mount::MsFlags::empty(), - None::<&str>, - )?; + // if let Some(disk) = manifest.disk.as_ref() { + // disk.apply(&loopdev.path().unwrap())?; + // disk.mount_to_chroot(&loopdev.path().unwrap(), &chroot)?; + // disk.unmount_from_chroot(&loopdev.path().unwrap(), &chroot)?; + // } - fs::create_dir_all(&chroot.join("boot/efi"))?; + let disk = manifest.clone().disk.unwrap(); + let ldp = loopdev.path().unwrap(); - nix::mount::mount( - Some(&efi_loopdev), - &chroot.join("boot/efi"), - Some("vfat"), - nix::mount::MsFlags::empty(), - None::<&str>, - )?; + // Partition disk - */ - self.root_builder.build(chroot.clone(), manifest)?; + disk.apply(&ldp)?; - // Now, after we finally have a rootfs, we can now run some post-install scripts + // Mount partitions to chroot - // reverse mount table, unmount + disk.mount_to_chroot(&ldp, &chroot)?; - for (_, path, _) in mount_table.iter().rev() { - nix::mount::umount(*path)?; - // nix::mount::umount(*ld)?; - } + self.root_builder.build(chroot.clone().canonicalize()?, manifest)?; + disk.unmount_from_chroot(&ldp, &chroot)?; loopdev.detach()?; Ok(()) diff --git a/src/config.rs b/src/config.rs index 728e132..9410229 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,9 @@ +use bytesize::ByteSize; use color_eyre::Result; use merge_struct::merge; use serde_derive::{Deserialize, Serialize}; -use tracing::{debug, trace}; -use std::path::PathBuf; +use std::{path::PathBuf, str::FromStr, fs}; +use tracing::{debug, trace, info}; #[derive(Deserialize, Debug, Clone, Serialize)] pub struct Manifest { @@ -19,6 +20,9 @@ pub struct Manifest { #[serde(default)] pub out_file: Option, + #[serde(default)] + pub disk: Option, + /// DNF configuration // todo: dynamically load this? pub dnf: crate::builder::DnfRootBuilder, @@ -68,15 +72,15 @@ impl Manifest { Ok(manifest) } - // pub fn list_all_imports(&self) -> Vec { - // let mut imports = Vec::new(); - // for import in self.import.clone() { - // let mut manifest = Self::load(import.clone()).unwrap(); - // imports.append(&mut manifest.list_all_imports()); - // imports.push(import); - // } - // imports - // } + /* pub fn list_all_imports(&self) -> Vec { + let mut imports = Vec::new(); + for import in self.import.clone() { + let mut manifest = Self::load(import.clone()).unwrap(); + imports.append(&mut manifest.list_all_imports()); + imports.push(import); + } + imports + } */ pub fn load_all(path: PathBuf) -> Result { // get all imports, then merge them all @@ -127,13 +131,386 @@ impl Script { } } +/// Utility function for determining partition /dev names +/// For cases where it's a mmcblk, or nvme, or loop device etc +pub fn partition_name(disk: &str, partition: u32) -> String { + let devname = if disk.starts_with("/dev/mmcblk") { + // mmcblk0p1 + format!("{}p{}", disk, partition) + } else if disk.starts_with("/dev/nvme") { + // nvme0n1p1 + format!("{}p{}", disk, partition) + } else if disk.starts_with("/dev/loop") { + // loop0p1 + format!("{}p{}", disk, partition) + } else { + // sda1 + format!("{}{}", disk, partition) + }; + + devname +} + +#[test] +fn test_dev_name() { + let devname = partition_name("/dev/mmcblk0", 1); + assert_eq!(devname, "/dev/mmcblk0p1"); + + let devname = partition_name("/dev/nvme0n1", 1); + assert_eq!(devname, "/dev/nvme0n1p1"); + + let devname = partition_name("/dev/loop0", 1); + assert_eq!(devname, "/dev/loop0p1"); + + let devname = partition_name("/dev/sda", 1); + assert_eq!(devname, "/dev/sda1"); +} + +#[derive(Deserialize, Debug, Clone, Serialize, PartialEq, Eq)] +pub struct PartitionLayout { + pub size: Option, + pub partitions: Vec, +} + +impl PartitionLayout { + pub fn new() -> Self { + Self { partitions: Vec::new(), size: None } + } + + /// Adds a partition to the layout + pub fn add_partition(&mut self, partition: Partition) { + self.partitions.push(partition); + } + + pub fn get_index(&self, mountpoint: &str) -> Option { + // index should be +1 of the actual partition number (sda1 is index 0) + + Some( + self.partitions.iter().position(|p| p.mountpoint == mountpoint).unwrap_or_default() + 1, + ) + } + + pub fn get_partition(&self, mountpoint: &str) -> Option<&Partition> { + self.partitions.iter().find(|p| p.mountpoint == mountpoint) + } + + pub fn sort_partitions(&mut self) { + // We should sort partitions by mountpoint, so that we can mount them in order + // In this case, from the least nested to the most nested, so count the number of slashes + + self.partitions.sort_by(|a, b| { + let a_slashes = a.mountpoint.matches('/').count(); + let b_slashes = b.mountpoint.matches('/').count(); + + a_slashes.cmp(&b_slashes) + }); + } + + pub fn mount_to_chroot(&self, disk: &PathBuf, chroot: &PathBuf) -> Result<()> { + // mount partitions to chroot + + // sort partitions by mountpoint + let mut ordered = self.clone(); + ordered.sort_partitions(); + + debug!(or = ?ordered, "Mounting partitions to chroot"); + + for part in &ordered.partitions { + let index = ordered.get_index(&part.mountpoint).unwrap(); + let devname = partition_name(&disk.to_string_lossy(), index as u32); + + // clean the mountpoint so we don't have the slash at the start + let mp_cleaned = part.mountpoint.trim_start_matches('/'); + let mountpoint = chroot.join(&mp_cleaned); + + trace!( + "mount {devname} {mountpoint}", + devname = devname, + mountpoint = mountpoint.to_string_lossy() + ); + + fs::create_dir_all(&mountpoint)?; + + cmd_lib::run_cmd!(mount ${devname} ${mountpoint} 2>&1)?; + } + + Ok(()) + } + + pub fn unmount_from_chroot(&self, disk: &PathBuf, chroot: &PathBuf) -> Result<()> { + // unmount partitions from chroot + + // sort partitions by mountpoint + let mut ordered = self.clone(); + ordered.sort_partitions(); + // reverse the order + ordered.partitions.reverse(); + + debug!(or = ?ordered, "Unmounting partitions from chroot"); + + for part in &ordered.partitions { + let index = ordered.get_index(&part.mountpoint).unwrap(); + let devname = partition_name(&disk.to_string_lossy(), index as u32); + + // clean the mountpoint so we don't have the slash at the start + let mp_cleaned = part.mountpoint.trim_start_matches('/'); + let mountpoint = chroot.join(&mp_cleaned); + + trace!( + "umount {devname} {mountpoint}", + devname = devname, + mountpoint = mountpoint.to_string_lossy() + ); + + cmd_lib::run_cmd!(umount -l ${mountpoint} 2>&1)?; + } + + Ok(()) + } + + /// Generate fstab entries for the partitions + pub fn fstab(&self, chroot: &PathBuf) -> Result { + // sort partitions by mountpoint + let mut ordered = self.clone(); + ordered.sort_partitions(); + + let mut fstab = String::new(); + + for part in &ordered.partitions { + + // get devname by finding from mount, instead of index because we won't be using it as much + let mountpoint = PathBuf::from(&part.mountpoint); + let mountpoint_chroot = part.mountpoint.trim_start_matches('/'); + let mountpoint_chroot = chroot.join(mountpoint_chroot); + + debug!(mountpoint = ?mountpoint, "Mountpoint of partition"); + debug!(mountpoint_chroot = ?mountpoint_chroot, "Mountpoint of partition in chroot"); + + let devname = cmd_lib::run_fun!(findmnt -n -o SOURCE ${mountpoint_chroot})?; + + + debug!(devname = ?devname, "Device name of partition"); + + // We will generate by UUID + + let uuid = cmd_lib::run_fun!(blkid -s UUID -o value ${devname})?; + + debug!(uuid = ?uuid, "UUID of partition"); + + // clean the mountpoint so we don't have the slash at the start + // let mp_cleaned = part.mountpoint.trim_start_matches('/'); + + let fsname = { + if part.filesystem == "efi" { + "vfat" + } else { + &part.filesystem + } + }; + + let fsck = if part.filesystem == "efi" { "0" } else { "2" }; + + let entry = format!( + "UUID={uuid}\t{mountpoint}\t{fsname}\tdefaults\t0\t{fsck}", + uuid = uuid, + mountpoint = mountpoint.to_string_lossy(), + fsname = fsname, + fsck = fsck + ); + + fstab.push_str(&entry); + fstab.push('\n'); + } + + Ok(fstab) + } + + pub fn apply(&self, disk: &PathBuf) -> Result<()> { + // This is a destructive operation, so we need to make sure we don't accidentally wipe the wrong disk + + info!("Applying partition layout to disk: {:#?}", disk); + + // format disk with GPT + + trace!("Formatting disk with GPT"); + trace!("parted -s {disk} mklabel gpt", disk = disk.to_string_lossy()); + cmd_lib::run_cmd!(parted -s ${disk} mklabel gpt 2>&1)?; + + // create partitions + + let mut last_end = 0; + + for (i, part) in self.partitions.iter().enumerate() { + trace!("Creating partition {}:", i + 1); + trace!("{:#?}", part); + + // get index of partition + let index = self.get_index(&part.mountpoint).unwrap(); + trace!("Index: {}", index); + + let devname = partition_name(&disk.to_string_lossy(), index as u32); + + let start_string = if i == 0 { + // create partition at start of disk + "1MiB".to_string() + } else { + // create partition after last partition + ByteSize::b(last_end).to_string_as(true).replace(" ", "") + }; + + let end_string = if let Some(size) = part.size { + // create partition with size + last_end += size.as_u64(); + + // remove space for partition table + ByteSize::b(last_end).to_string_as(true).replace(" ", "") + } else { + // create partition at end of disk + "100%".to_string() + }; + + let part_fs_parted = if part.filesystem == "efi" { "fat32" } else { "ext4" }; + + trace!( + "parted -s {disk} mkpart primary {part_fs} {start} {end}", + disk = disk.to_string_lossy(), + part_fs = part_fs_parted, + start = start_string, + end = end_string + ); + + cmd_lib::run_cmd!(parted -s ${disk} mkpart primary ${part_fs_parted} ${start_string} ${end_string} 2>&1)?; + + if part.filesystem == "efi" { + trace!( + "parted -s {disk} set {index} esp on", + disk = disk.to_string_lossy(), + index = index + ); + + cmd_lib::run_cmd!(parted -s ${disk} set ${index} esp on 2>&1)?; + } + + + if let Some(label) = &part.label { + trace!( + "parted -s {disk} name {index} {label}", + disk = disk.to_string_lossy(), + index = index, + label = label + ); + + cmd_lib::run_cmd!(parted -s ${disk} name ${index} ${label} 2>&1)?; + } + + // time to format the filesystem + + let fsname = { + if part.filesystem == "efi" { + "fat" + } else { + &part.filesystem + } + }; + + // Some stupid hackery checks for the args of mkfs.fat + if part.filesystem == "efi" { + trace!( + "mkfs.fat -F32 {devname}", + devname = devname + ); + + cmd_lib::run_cmd!(mkfs.fat -F32 ${devname} 2>&1)?; + } else { + trace!( + "mkfs.{fs} {devname}", + fs = fsname, + devname = devname + ); + + cmd_lib::run_cmd!(mkfs.${fsname} ${devname} 2>&1)?; + } + + // create partition + trace!("===================="); + } + + Ok(()) + } +} + #[test] -fn test_recurse() { - // cd tests/ng/recurse +fn test_partlay() { + // Partition layout test + + let mock_disk = PathBuf::from("/dev/sda"); + + let mut partlay = PartitionLayout::new(); + + partlay.add_partition(Partition { + label: Some("EFI".to_string()), + size: Some(ByteSize::mib(100)), + filesystem: "efi".to_string(), + mountpoint: "/boot/efi".to_string(), + }); + + partlay.add_partition(Partition { + label: Some("ROOT".to_string()), + size: Some(ByteSize::gib(100)), + filesystem: "ext4".to_string(), + mountpoint: "/".to_string(), + }); - let manifest = Manifest::load_all(PathBuf::from("tests/ng/recurse/manifest.yaml")).unwrap(); + partlay.add_partition(Partition { + label: Some("HOME".to_string()), + size: Some(ByteSize::gib(100)), + filesystem: "ext4".to_string(), + mountpoint: "/home".to_string(), + }); - println!("{manifest:#?}"); + for (i, part) in partlay.partitions.iter().enumerate() { + println!("Partition {}:", i); + println!("{:#?}", part); - // let ass: Manifest = Manifest { import: vec!["recurse1.yaml", "recurse2.yaml"], distro: Some("RecursiveOS"), out_file: None, dnf: (), scripts: () } + // get index of partition + let index = partlay.get_index(&part.mountpoint).unwrap(); + println!("Index: {}", index); + + println!("Partition name: {}", partition_name(&mock_disk.to_string_lossy(), index as u32)); + + println!("===================="); + } + + partlay.apply(&mock_disk).unwrap(); + // check if parts would be applied correctly +} + +#[derive(Deserialize, Debug, Clone, Serialize, PartialEq, Eq)] +pub struct Partition { + pub label: Option, + // If not specified, the partition will be created at the end of the disk (100%) + pub size: Option, + /// Filesystem of the partition + pub filesystem: String, + /// The mountpoint of the partition + pub mountpoint: String, } + +#[test] +fn test_bytesize() { + let size = ByteSize::mib(100); + println!("{:#?}", size); + + let size = ByteSize::from_str("100M").unwrap(); + println!("{:#?}", size.as_u64()) +} +// #[test] +// fn test_recurse() { +// // cd tests/ng/recurse + +// let manifest = Manifest::load_all(PathBuf::from("tests/ng/recurse/manifest.yaml")).unwrap(); + +// println!("{manifest:#?}"); + +// // let ass: Manifest = Manifest { import: vec!["recurse1.yaml", "recurse2.yaml"], distro: Some("RecursiveOS"), out_file: None, dnf: (), scripts: () } +// } diff --git a/tests/ng/katsu.yaml b/tests/ng/katsu.yaml index 62b4f7d..c098e1d 100644 --- a/tests/ng/katsu.yaml +++ b/tests/ng/katsu.yaml @@ -30,6 +30,19 @@ scripts: name: Relabel SELinux for new filesystem file: selinux.sh +disk: + size: 4G + partitions: + - label: EFI + size: 512MiB + filesystem: efi + mountpoint: /boot/efi + - label: root + # size: 2.5MiB + filesystem: ext4 + mountpoint: / + + dnf: releasever: 38 packages: