// bootc-image-builder: A tool to convert bootc container images to bootable disk images. // // This program creates bootable disk images from bootc-compatible container images. // It handles the complete bootc workflow including OSTree integration, composefs setup, // initramfs creation, and bootloader installation. // // Features: // - Converts bootc container images to QCOW2, raw, VMDK, and ISO formats // - Sets up OSTree repository and composefs configuration // - Creates initramfs with bootc binary support // - Installs GRUB or systemd-boot bootloaders // - Supports UEFI and BIOS boot modes // - Handles secure boot configuration // // Dependencies: // - podman (container runtime) // - qemu-img (disk image creation) // - dracut (initramfs creation) // - grub2-efi or systemd-boot (bootloader) // - ostree (OSTree repository management) // - composefs (container filesystem) // // To compile and run this program, you will need the following dependencies in your // `Cargo.toml` file: // // [dependencies] // clap = { version = "4", features = ["derive"] } // anyhow = "1.0" // tempfile = "3.10" // serde = { version = "1.0", features = ["derive"] } // serde_json = "1.0" // oci-spec = "0.6" // log = "0.4" // pretty_env_logger = "0.5" use clap::Parser; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::io::Write; use tempfile::{tempdir, tempdir_in}; use anyhow::{Result, Context}; use serde::{Deserialize, Serialize}; use log::{error, info, warn}; use std::os::unix::fs::PermissionsExt; /// A tool to convert bootc container images into bootable disk images. #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { /// The name of the bootc container image to build from (e.g., 'localhost/my-debian-server:latest') #[arg(short, long)] image: Option, /// The format of the output disk image (qcow2, raw, vmdk, iso, ami) #[arg(short, long, default_value = "qcow2")] format: ImageFormat, /// The path to save the generated disk image file #[arg(short, long, default_value = "bootc-image")] output: PathBuf, /// The size of the disk image in GB #[arg(short, long, default_value_t = 10)] size: u32, /// The architecture to build for (x86_64, aarch64, ppc64le) #[arg(long, default_value = "x86_64")] arch: String, /// The bootloader to use (grub, systemd-boot, unified-kernel-image, efi-boot-stub, clover) #[arg(long, default_value = "grub")] bootloader: BootloaderType, /// Partitioning tool to use (parted, sgdisk, sfdisk, auto) #[arg(long, default_value = "auto")] partitioner: String, /// Use local rootfs directory instead of container image #[arg(long)] rootfs: Option, /// Enable secure boot support #[arg(long)] secure_boot: bool, /// Enable UEFI boot (default: auto-detect) #[arg(long)] uefi: bool, /// Enable BIOS boot (default: auto-detect) #[arg(long)] bios: bool, /// Custom kernel command line arguments #[arg(long, default_value = "console=ttyS0,115200n8 quiet")] kernel_args: String, /// Cloud provider for cloud-specific optimizations #[arg(long)] cloud_provider: Option, /// Test the created disk image with QEMU #[arg(long)] test_with_qemu: bool, /// QEMU timeout in seconds for boot testing #[arg(long, default_value_t = 30)] qemu_timeout: u64, } #[derive(Debug, Clone, clap::ValueEnum)] enum ImageFormat { Qcow2, Raw, Vmdk, Iso, Ami, } #[derive(Debug, Clone, clap::ValueEnum)] enum BootloaderType { Grub, SystemdBoot, // Future bootloader support (stub implementations) UnifiedKernelImage, // UKI EfiBootStub, // EFI boot stub Clover, // Clover bootloader } #[derive(Debug, Clone, clap::ValueEnum)] enum CloudProvider { Aws, Azure, Gcp, } #[derive(Debug, Serialize, Deserialize)] struct BootcConfig { container_image: String, ostree_repo: String, composefs_enabled: bool, bootloader: String, secure_boot: bool, kernel_args: String, } fn main() -> Result<()> { // Initialize logging pretty_env_logger::init(); info!("Starting bootc-image-builder"); // Parse command-line arguments let args = Args::parse(); // Validate arguments if args.image.is_none() && args.rootfs.is_none() { return Err(anyhow::anyhow!("Either --image or --rootfs must be specified")); } let image_desc = if let Some(ref img) = args.image { format!("container image: {}", img) } else { format!("rootfs directory: {}", args.rootfs.as_ref().unwrap()) }; info!("Building bootc image from {} -> {:?}", image_desc, args.output); // Create temporary working directory with more space let temp_dir = tempdir_in("/home/joe/Projects/overwatch/tmp") .or_else(|_| tempdir()) .context("Failed to create temporary directory")?; let temp_path = temp_dir.path(); info!("Created temporary directory at: {:?}", temp_path); // Build the bootc image let image_path = build_bootc_image(&args, temp_path).context("Failed to build bootc image")?; info!("Successfully built bootc image at: {}", image_path.display()); println!("✅ Bootc image created: {}", image_path.display()); // Test with QEMU if requested if args.test_with_qemu { info!("Testing disk image with QEMU..."); test_disk_image_with_qemu(&image_path, args.qemu_timeout)?; } println!("🚀 You can now boot this image in QEMU, VMware, or deploy to cloud!"); Ok(()) } /// Main function that orchestrates the complete bootc image building process fn build_bootc_image(args: &Args, temp_path: &Path) -> Result { // Step 1: Get rootfs - either from container image or local directory let rootfs_path = if let Some(rootfs_dir) = &args.rootfs { info!("Step 1: Using local rootfs directory"); let rootfs_path = PathBuf::from(rootfs_dir); if !rootfs_path.exists() { return Err(anyhow::anyhow!("Rootfs directory does not exist: {}", rootfs_dir)); } info!("Using rootfs directory: {}", rootfs_path.display()); rootfs_path } else { info!("Step 1: Pulling and extracting container image"); let image = args.image.as_ref().unwrap(); pull_and_extract_image(image, temp_path) .context("Failed to pull and extract image")? }; // Step 1.5: Use user-specified bootloader or auto-detect info!("Step 1.5: Bootloader configuration"); let bootloader = &args.bootloader; info!("Using bootloader: {:?}", bootloader); // Step 2: Set up bootc support info!("Step 2: Setting up bootc support"); setup_bootc_support(&rootfs_path, args) .context("Failed to setup bootc support")?; // Step 3: Create OSTree repository info!("Step 3: Creating OSTree repository"); let ostree_repo_path = create_ostree_repository(&rootfs_path, temp_path) .context("Failed to create OSTree repository")?; // Step 4: Configure composefs info!("Step 4: Configuring composefs"); configure_composefs(&rootfs_path, &ostree_repo_path) .context("Failed to configure composefs")?; // Step 5: Create initramfs with bootc support info!("Step 5: Creating initramfs with bootc support"); create_bootc_initramfs(&rootfs_path, args) .context("Failed to create bootc initramfs")?; // Step 6: Install bootloader (use user's choice) info!("Step 6: Installing bootloader"); install_bootloader_with_type(&rootfs_path, args, &bootloader) .context("Failed to install bootloader")?; // Step 7: Create disk image info!("Step 7: Creating disk image"); let image_path = create_disk_image(&rootfs_path, &args.output, &args.format, args.size, &bootloader, &args.partitioner, &args) .context("Failed to create disk image")?; Ok(image_path) } /// Sets up bootc support in the root filesystem fn setup_bootc_support(rootfs_path: &Path, args: &Args) -> Result<()> { info!("Setting up bootc support in rootfs"); // Create necessary directories let bootc_dirs = [ "usr/bin", "usr/lib/ostree", "ostree", "sysroot", ]; for dir in &bootc_dirs { let path = rootfs_path.join(dir); fs::create_dir_all(&path) .with_context(|| format!("Failed to create directory: {}", path.display()))?; } // Install bootc binary (try real binary first, fallback to script) let bootc_binary = rootfs_path.join("usr/bin/bootc"); // Try to download and install real bootc binary if let Err(e) = download_and_install_bootc_binary(&bootc_binary) { warn!("Failed to download real bootc binary: {}", e); warn!("Falling back to placeholder script"); // Fallback to placeholder script fs::write(&bootc_binary, create_bootc_script()) .context("Failed to create bootc binary")?; } else { info!("Successfully installed real bootc binary"); } // Make bootc executable let mut perms = fs::metadata(&bootc_binary)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&bootc_binary, perms) .context("Failed to set bootc executable permissions")?; // Set up /sbin/init -> /usr/bin/bootc let init_link = rootfs_path.join("sbin/init"); if init_link.exists() { fs::remove_file(&init_link) .context("Failed to remove existing /sbin/init")?; } std::os::unix::fs::symlink("/usr/bin/bootc", &init_link) .context("Failed to create /sbin/init symlink")?; // Create bootc configuration let config = BootcConfig { container_image: args.image.clone().unwrap_or_else(|| "local-rootfs".to_string()), ostree_repo: "/ostree/repo".to_string(), composefs_enabled: true, bootloader: format!("{:?}", args.bootloader).to_lowercase(), secure_boot: args.secure_boot, kernel_args: args.kernel_args.clone(), }; let config_path = rootfs_path.join("etc/bootc.conf"); let config_json = serde_json::to_string_pretty(&config) .context("Failed to serialize bootc config")?; fs::write(&config_path, config_json) .context("Failed to write bootc config")?; info!("Bootc support configured successfully"); Ok(()) } /// Creates an OSTree repository fn create_ostree_repository(rootfs_path: &Path, temp_path: &Path) -> Result { info!("Creating OSTree repository"); let ostree_repo_path = temp_path.join("ostree-repo"); fs::create_dir_all(&ostree_repo_path) .context("Failed to create OSTree repository directory")?; // Initialize OSTree repository let output = Command::new("ostree") .arg("init") .arg("--repo") .arg(&ostree_repo_path) .arg("--mode=bare") .output() .context("Failed to initialize OSTree repository")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("ostree init failed: {}", stderr)); } // Debug: Check if rootfs directory exists and list contents if !rootfs_path.exists() { return Err(anyhow::anyhow!("Rootfs directory does not exist: {:?}", rootfs_path)); } let entries = fs::read_dir(rootfs_path) .context("Failed to read rootfs directory")?; let entry_count = entries.count(); info!("Rootfs directory has {} entries", entry_count); // Create a commit from the rootfs (ensure absolute path) let rootfs_abs_path = rootfs_path.canonicalize() .context("Failed to get absolute path for rootfs")?; info!("Creating OSTree commit from: {:?}", rootfs_abs_path); // Verify the directory exists and is readable if !rootfs_abs_path.exists() { return Err(anyhow::anyhow!("Rootfs directory does not exist: {:?}", rootfs_abs_path)); } let metadata = fs::metadata(&rootfs_abs_path) .context("Failed to get rootfs metadata")?; if !metadata.is_dir() { return Err(anyhow::anyhow!("Rootfs path is not a directory: {:?}", rootfs_abs_path)); } // Create a temporary copy of rootfs without problematic directories let clean_rootfs_path = temp_path.join("clean-rootfs"); if clean_rootfs_path.exists() { fs::remove_dir_all(&clean_rootfs_path) .context("Failed to remove existing clean rootfs")?; } fs::create_dir_all(&clean_rootfs_path) .context("Failed to create clean rootfs directory")?; // Copy rootfs excluding problematic directories let output = Command::new("rsync") .arg("-a") .arg("--exclude=/dev") .arg("--exclude=/proc") .arg("--exclude=/sys") .arg("--exclude=/tmp") .arg("--exclude=/run") .arg("--exclude=/var/cache") .arg("--exclude=/var/tmp") .arg(format!("{}/", rootfs_abs_path.display())) .arg(format!("{}/", clean_rootfs_path.display())) .output() .context("Failed to copy clean rootfs")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("rsync failed: {}", stderr)); } let output = Command::new("ostree") .arg("commit") .arg("--repo") .arg(&ostree_repo_path) .arg("--branch=main") .arg(format!("--tree=dir={}", clean_rootfs_path.display())) .arg("--add-metadata-string=version=1.0") .arg("--add-metadata-string=title=Bootc Image") .output() .context("Failed to create OSTree commit")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("ostree commit failed: {}", stderr)); } // Copy repository to rootfs let rootfs_ostree = rootfs_path.join("ostree/repo"); fs::create_dir_all(&rootfs_ostree) .context("Failed to create /ostree/repo in rootfs")?; let output = Command::new("sh") .arg("-c") .arg(format!("cp -r {}/* {}", ostree_repo_path.display(), rootfs_ostree.display())) .output() .context("Failed to copy OSTree repository to rootfs")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("cp failed: {}", stderr)); } info!("OSTree repository created successfully"); Ok(ostree_repo_path) } /// Configures composefs for the bootc image fn configure_composefs(rootfs_path: &Path, ostree_repo_path: &Path) -> Result<()> { info!("Configuring composefs"); // Create prepare-root.conf let prepare_root_conf = rootfs_path.join("usr/lib/ostree/prepare-root.conf"); let conf_content = format!( "[composefs]\n\ enabled = yes\n\ store = {}\n", ostree_repo_path.display() ); fs::write(&prepare_root_conf, conf_content) .context("Failed to write prepare-root.conf")?; // Create composefs flag files let composefs_flag = rootfs_path.join("usr/lib/ostree/composefs"); fs::write(&composefs_flag, "1") .context("Failed to create composefs flag file")?; let ostree_composefs_flag = rootfs_path.join("ostree/composefs"); fs::create_dir_all(ostree_composefs_flag.parent().unwrap()) .context("Failed to create /ostree directory")?; fs::write(&ostree_composefs_flag, "1") .context("Failed to create /ostree/composefs flag file")?; info!("Composefs configured successfully"); Ok(()) } /// Creates initramfs with bootc support using dracut fn create_bootc_initramfs(rootfs_path: &Path, args: &Args) -> Result<()> { info!("Creating bootc initramfs with dracut"); // Create dracut configuration let dracut_conf = rootfs_path.join("etc/dracut.conf.d/bootc.conf"); fs::create_dir_all(dracut_conf.parent().unwrap()) .context("Failed to create dracut config directory")?; let dracut_content = format!( "add_dracutmodules+=\"bootc\"\n\ install_items+=\"bootc /usr/bin/bootc\"\n\ kernel_cmdline=\"{}\"\n", args.kernel_args ); fs::write(&dracut_conf, dracut_content) .context("Failed to write dracut config")?; // Create bootc dracut module let dracut_modules_dir = rootfs_path.join("usr/lib/dracut/modules.d/90bootc"); fs::create_dir_all(&dracut_modules_dir) .context("Failed to create dracut modules directory")?; let module_script = create_bootc_dracut_module(); fs::write(dracut_modules_dir.join("module-setup.sh"), module_script) .context("Failed to write bootc dracut module")?; // Ensure boot directory exists let boot_dir = rootfs_path.join("boot"); fs::create_dir_all(&boot_dir) .context("Failed to create boot directory")?; // Create a minimal kernel stub (in real implementation, this would be a real kernel) let kernel_path = rootfs_path.join("boot/vmlinuz"); create_kernel_stub(&kernel_path) .context("Failed to create kernel stub")?; // Run dracut to create initramfs let initramfs_path = rootfs_path.join("boot/initramfs-bootc.img"); let output = Command::new("dracut") .arg("--force") .arg("--no-hostonly") .arg("--reproducible") .arg("--zstd") .arg("--kver") .arg("5.15.0") // This should be detected from the kernel .arg(&initramfs_path) .current_dir(rootfs_path) .output() .context("Failed to run dracut")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); warn!("Dracut failed, creating minimal initramfs: {}", stderr); create_minimal_initramfs(rootfs_path, &initramfs_path)?; } info!("Bootc initramfs created successfully"); Ok(()) } /// Installs the bootloader (GRUB or systemd-boot) fn install_bootloader(rootfs_path: &Path, args: &Args) -> Result<()> { info!("Installing bootloader: {:?}", args.bootloader); match args.bootloader { BootloaderType::Grub => install_grub_bootloader(rootfs_path, args)?, BootloaderType::SystemdBoot => install_systemd_bootloader(rootfs_path, args)?, // Future bootloader support (stub implementations) BootloaderType::UnifiedKernelImage => install_uki_bootloader(rootfs_path, args)?, BootloaderType::EfiBootStub => install_efi_boot_stub_bootloader(rootfs_path, args)?, BootloaderType::Clover => install_clover_bootloader(rootfs_path, args)?, } info!("Bootloader installed successfully"); Ok(()) } /// Creates the final disk image fn create_disk_image(rootfs_path: &Path, output_path: &Path, format: &ImageFormat, size_gb: u32, bootloader_type: &BootloaderType, partitioner: &str, args: &Args) -> Result { info!("Creating disk image in {:?} format", format); let raw_image_path = output_path.with_extension("raw"); // Create raw disk image let output = Command::new("qemu-img") .arg("create") .arg("-f") .arg("raw") .arg(&raw_image_path) .arg(format!("{}G", size_gb)) .output() .context("Failed to create raw disk image")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("qemu-img create failed: {}", stderr)); } // Partition and format the disk partition_and_format_disk(&raw_image_path, rootfs_path, bootloader_type, partitioner, &args)?; // Convert to final format if needed let final_image_path = match format { ImageFormat::Raw => raw_image_path, _ => { let final_path = output_path.with_extension(format!("{:?}", format).to_lowercase()); convert_disk_format(&raw_image_path, &final_path, format)?; // Keep raw image for debugging // fs::remove_file(&raw_image_path).context("Failed to remove temporary raw image")?; final_path } }; info!("Disk image created: {}", final_image_path.display()); Ok(final_image_path) } /// Pulls and extracts the container image filesystem into a temporary directory. fn pull_and_extract_image(image_name: &str, temp_path: &Path) -> Result { info!("Pulling and extracting container image: '{}'", image_name); // Create a path for the tarball and the extracted filesystem. let tarball_path = temp_path.join("image.tar"); let extracted_path = temp_path.join("rootfs"); fs::create_dir_all(&extracted_path).context("Failed to create rootfs directory")?; // Use `podman` to save the container image as a tarball. let output = Command::new("podman") .arg("save") .arg("--format=oci-archive") .arg("-o") .arg(&tarball_path) .arg(image_name) .output() .context("Failed to run 'podman save'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'podman save' failed with:\n{}", stderr)); } println!("Image saved as tarball: {:?}", tarball_path); // Use `tar` to extract the contents of the tarball. let output = Command::new("tar") .arg("-xf") .arg(&tarball_path) .arg("-C") .arg(&extracted_path) .output() .context("Failed to run 'tar xf'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'tar xf' failed with:\n{}", stderr)); } println!("Image extracted to: {:?}", extracted_path); // Debug: List contents of extracted directory let entries = fs::read_dir(&extracted_path) .context("Failed to read extracted directory")?; for entry in entries { let entry = entry?; println!("Found: {:?}", entry.file_name()); } // Parse the OCI index to get manifest information let index_path = extracted_path.join("index.json"); let index_content = fs::read_to_string(&index_path) .context("Failed to read index.json")?; let index: serde_json::Value = serde_json::from_str(&index_content) .context("Failed to parse index.json")?; // Get the manifest digest from the index let manifest_digest = index["manifests"][0]["digest"] .as_str() .ok_or_else(|| anyhow::anyhow!("No manifest digest found in index"))?; // Remove the "sha256:" prefix if present let manifest_digest = manifest_digest.strip_prefix("sha256:").unwrap_or(manifest_digest); let manifest_path = extracted_path.join("blobs").join("sha256").join(manifest_digest); let manifest_content = fs::read_to_string(&manifest_path) .context("Failed to read manifest from blobs")?; let manifest: serde_json::Value = serde_json::from_str(&manifest_content) .context("Failed to parse manifest")?; // Get the layers array from the manifest let layers = manifest["layers"] .as_array() .ok_or_else(|| anyhow::anyhow!("No layers found in manifest"))?; info!("Found {} layers in OCI image", layers.len()); // Create a new directory for the final root filesystem let final_rootfs_path = temp_path.join("final_rootfs"); fs::create_dir_all(&final_rootfs_path).context("Failed to create final rootfs directory")?; // Extract each layer in order (layers are stacked on top of each other) for (i, layer) in layers.iter().enumerate() { let layer_digest = layer["digest"] .as_str() .ok_or_else(|| anyhow::anyhow!("Invalid layer digest in manifest"))?; // Remove the "sha256:" prefix if present let layer_digest = layer_digest.strip_prefix("sha256:").unwrap_or(layer_digest); let layer_path = extracted_path.join("blobs").join("sha256").join(layer_digest); if !layer_path.exists() { return Err(anyhow::anyhow!("Layer file not found: {}", layer_digest)); } info!("Extracting layer {}/{}: {}", i + 1, layers.len(), layer_digest); // Extract the layer tarball let output = Command::new("tar") .arg("-xf") .arg(&layer_path) .arg("-C") .arg(&final_rootfs_path) .output() .context(format!("Failed to extract layer {}", layer_digest))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to extract layer {}: {}", layer_digest, stderr)); } } println!("Final root filesystem extracted to: {:?}", final_rootfs_path); Ok(final_rootfs_path) } /// Creates a disk image, partitions it, formats it, and copies the filesystem. /// This function requires `sudo` privileges to run. fn create_and_copy_to_disk( rootfs_path: &Path, output_path: &Path, format: &str, size_gb: u32, ) -> Result { println!("Creating disk image..."); let image_path = output_path.with_extension(format); // 1. Create a sparse raw disk image. let output = Command::new("qemu-img") .arg("create") .arg("-f") .arg("raw") .arg(&image_path) .arg(format!("{}G", size_gb)) .output() .context("Failed to run 'qemu-img create'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'qemu-img create' failed with:\n{}", stderr)); } println!("Raw image created at: {}", image_path.display()); // 2. Use `kpartx` or `fdisk` to create a loop device and partition the image. // NOTE: This part is highly dependent on system tools and privileges. // A robust tool would handle this more carefully, but for this example, we will // create a simple partition table with fdisk. println!("Partitioning the disk image..."); let parted_script = format!( "n\np\n1\n\n\nw\n" ); let output = Command::new("sudo") .arg("/sbin/fdisk") .arg(&image_path) .arg(parted_script) .output() .context("Failed to run 'fdisk'")?; // We can't easily capture the output of fdisk in a non-interactive way with this script. // We will assume success for this example and print a warning. if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); eprintln!("'fdisk' failed (this is often a permission or interactive issue). Stderr:\n{}", stderr); // The rest of the process will likely fail if this command failed. // For demonstration, we continue, but in a real tool, this would be an error. } println!("Partitions created."); // 3. Create a loop device for the disk image. // NOTE: This step is a common pain point for automation without `kpartx`. // We will use `losetup` if available, otherwise assume a manual step. println!("Setting up loop device..."); let output = Command::new("sudo") .arg("losetup") .arg("-f") .arg("--show") .arg(&image_path) .output() .context("Failed to run 'losetup'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'losetup' failed with:\n{}", stderr)); } let loop_device = String::from_utf8_lossy(&output.stdout).trim().to_string(); println!("Loop device created at: {}", loop_device); // 4. Format the filesystem on the partition. let partition_device = format!("{}p1", loop_device); println!("Formatting partition: {}...", partition_device); let output = Command::new("sudo") .arg("mkfs.ext4") .arg("-F") .arg(&partition_device) .output() .context("Failed to run 'mkfs.ext4'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'mkfs.ext4' failed with:\n{}", stderr)); } println!("Partition formatted successfully."); // 5. Mount the new filesystem. let temp_dir = tempdir().context("Failed to create temporary directory")?; let mount_dir = temp_dir.path().join("mnt"); fs::create_dir_all(&mount_dir).context("Failed to create mount directory")?; println!("Mounting to: {}", mount_dir.display()); let output = Command::new("sudo") .arg("mount") .arg(&partition_device) .arg(&mount_dir) .output() .context("Failed to run 'mount'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'mount' failed with:\n{}", stderr)); } // 6. Copy the root filesystem into the mounted partition. println!("Copying root filesystem to mounted partition..."); let output = Command::new("sudo") .arg("cp") .arg("-a") .arg(rootfs_path.to_str().ok_or_else(|| anyhow::anyhow!("Invalid rootfs path"))?) .arg(mount_dir.to_str().ok_or_else(|| anyhow::anyhow!("Invalid mount path"))?) .output() .context("Failed to run 'cp -a'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'cp -a' failed with:\n{}", stderr)); } // 7. Unmount the filesystem and detach the loop device. println!("Unmounting filesystem and cleaning up..."); let output = Command::new("sudo") .arg("umount") .arg(&mount_dir) .output() .context("Failed to run 'umount'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'umount' failed with:\n{}", stderr)); } let output = Command::new("sudo") .arg("losetup") .arg("-d") .arg(&loop_device) .output() .context("Failed to run 'losetup -d'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'losetup -d' failed with:\n{}", stderr)); } // 8. Convert the raw image to the requested format (if not raw). if format != "raw" { println!("Converting raw image to {}...", format); let final_image_path = output_path.with_extension(format); let output = Command::new("qemu-img") .arg("convert") .arg("-f") .arg("raw") .arg("-O") .arg(format) .arg(&image_path) .arg(&final_image_path) .output() .context("Failed to run 'qemu-img convert'")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("'qemu-img convert' failed with:\n{}", stderr)); } println!("Image converted to: {}", final_image_path.display()); fs::remove_file(&image_path).context("Failed to remove temporary raw image")?; return Ok(final_image_path); } Ok(image_path) } /// Downloads and installs the real bootc binary to a specific location fn download_and_install_bootc_binary(bootc_binary_path: &Path) -> Result<()> { info!("Downloading real bootc binary from registry"); // Create a temporary directory for the download let temp_dir = tempdir().context("Failed to create temporary directory for bootc download")?; let download_dir = temp_dir.path().join("bootc-download"); fs::create_dir_all(&download_dir) .context("Failed to create download directory")?; // Download the bootc package from the registry let package_url = "https://git.raines.xyz/particle-os/-/packages/debian/bootc/0.1.0++/download"; let package_path = download_dir.join("bootc.deb"); info!("Downloading bootc package from: {}", package_url); let output = Command::new("curl") .arg("-L") .arg("-o") .arg(&package_path) .arg(package_url) .output() .context("Failed to download bootc package")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to download bootc package: {}", stderr)); } // Extract the package info!("Extracting bootc package"); let extract_dir = download_dir.join("extract"); fs::create_dir_all(&extract_dir) .context("Failed to create extract directory")?; let output = Command::new("dpkg-deb") .arg("-x") .arg(&package_path) .arg(&extract_dir) .output() .context("Failed to extract bootc package")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to extract bootc package: {}", stderr)); } // Copy the bootc binary to the final location let bootc_binary_src = extract_dir.join("usr/bin/bootc"); if !bootc_binary_src.exists() { return Err(anyhow::anyhow!("Bootc binary not found in extracted package")); } fs::copy(&bootc_binary_src, bootc_binary_path) .context("Failed to copy bootc binary to final location")?; info!("Bootc binary downloaded and installed successfully"); Ok(()) } /// Downloads and installs the real bootc binary from the registry fn download_bootc_binary(temp_path: &Path) -> Result<()> { info!("Downloading real bootc binary from registry"); // Create a temporary directory for the download let download_dir = temp_path.join("bootc-download"); fs::create_dir_all(&download_dir) .context("Failed to create download directory")?; // Download the bootc package from the registry let package_url = "https://git.raines.xyz/particle-os/-/packages/debian/bootc/0.1.0++/download"; let package_path = download_dir.join("bootc.deb"); info!("Downloading bootc package from: {}", package_url); let output = Command::new("curl") .arg("-L") .arg("-o") .arg(&package_path) .arg(package_url) .output() .context("Failed to download bootc package")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to download bootc package: {}", stderr)); } // Extract the package info!("Extracting bootc package"); let extract_dir = download_dir.join("extract"); fs::create_dir_all(&extract_dir) .context("Failed to create extract directory")?; let output = Command::new("dpkg-deb") .arg("-x") .arg(&package_path) .arg(&extract_dir) .output() .context("Failed to extract bootc package")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to extract bootc package: {}", stderr)); } // Copy the bootc binary to the final location let bootc_binary_src = extract_dir.join("usr/bin/bootc"); let bootc_binary_dst = temp_path.join("bootc-binary"); if !bootc_binary_src.exists() { return Err(anyhow::anyhow!("Bootc binary not found in extracted package")); } fs::copy(&bootc_binary_src, &bootc_binary_dst) .context("Failed to copy bootc binary")?; // Make it executable let mut perms = fs::metadata(&bootc_binary_dst)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&bootc_binary_dst, perms) .context("Failed to set bootc binary permissions")?; info!("Bootc binary downloaded and prepared successfully"); Ok(()) } /// Creates a bootc script (placeholder implementation) fn create_bootc_script() -> String { r#"#!/bin/bash # Bootc binary - placeholder implementation # In a real implementation, this would be the actual bootc binary set -euo pipefail echo "Bootc: Starting container boot process..." # Read configuration CONFIG_FILE="/etc/bootc.conf" if [[ -f "$CONFIG_FILE" ]]; then source "$CONFIG_FILE" fi # Set up composefs if [[ "${composefs_enabled:-yes}" == "yes" ]]; then echo "Bootc: Setting up composefs..." # This would use ostree-ext-container in a real implementation echo "Bootc: Composefs setup complete" fi # Mount the container filesystem echo "Bootc: Mounting container filesystem..." # This would mount the container as the root filesystem # Execute the real init echo "Bootc: Executing real init..." exec /sbin/systemd "#.to_string() } /// Creates a bootc dracut module fn create_bootc_dracut_module() -> String { r#"#!/bin/bash # Bootc dracut module check() { return 0 } depends() { echo systemd return 0 } install() { inst /usr/bin/bootc inst /etc/bootc.conf inst /usr/lib/ostree/prepare-root.conf inst_hook cmdline 30 "$moddir/bootc-cmdline.sh" inst_hook initqueue/settled 30 "$moddir/bootc-init.sh" } installkernel() { return 0 } "#.to_string() } /// Creates a minimal initramfs if dracut fails fn create_minimal_initramfs(rootfs_path: &Path, initramfs_path: &Path) -> Result<()> { info!("Creating minimal initramfs"); // Create a minimal initramfs with just the bootc binary let temp_initramfs = rootfs_path.join("tmp/initramfs"); fs::create_dir_all(&temp_initramfs) .context("Failed to create temporary initramfs directory")?; // Copy bootc binary let bootc_src = rootfs_path.join("usr/bin/bootc"); let bootc_dst = temp_initramfs.join("bootc"); if bootc_src.exists() { fs::copy(&bootc_src, &bootc_dst) .context("Failed to copy bootc binary to initramfs")?; } // Create init script let init_script = temp_initramfs.join("init"); fs::write(&init_script, "#!/bin/sh\nexec /bootc\n") .context("Failed to create init script")?; let mut perms = fs::metadata(&init_script)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&init_script, perms) .context("Failed to set init script permissions")?; // Create initramfs archive using shell let initramfs_cmd = format!( "find {} -print0 | cpio -o -H newc --null | zstd -c > {}", temp_initramfs.display(), initramfs_path.display() ); let output = Command::new("sh") .arg("-c") .arg(&initramfs_cmd) .output() .context("Failed to create minimal initramfs")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to create minimal initramfs: {}", stderr)); } info!("Minimal initramfs created successfully"); Ok(()) } /// Installs GRUB bootloader fn install_grub_bootloader(rootfs_path: &Path, args: &Args) -> Result<()> { info!("Installing GRUB bootloader"); // Create GRUB configuration let grub_cfg = rootfs_path.join("boot/grub/grub.cfg"); fs::create_dir_all(grub_cfg.parent().unwrap()) .context("Failed to create GRUB directory")?; let grub_content = format!( r#"set default=0 set timeout=5 menuentry "Bootc Image" {{ linux /boot/vmlinuz {} initrd /boot/initramfs-bootc.img }} "#, args.kernel_args ); fs::write(&grub_cfg, grub_content) .context("Failed to write GRUB configuration")?; info!("GRUB bootloader configured"); Ok(()) } /// Installs systemd-boot bootloader fn install_systemd_bootloader(rootfs_path: &Path, args: &Args) -> Result<()> { info!("Installing systemd-boot bootloader"); // Create systemd-boot configuration let boot_entries_dir = rootfs_path.join("boot/loader/entries"); fs::create_dir_all(&boot_entries_dir) .context("Failed to create boot entries directory")?; let boot_entry = boot_entries_dir.join("bootc.conf"); let entry_content = format!( r#"title Bootc Image linux /boot/vmlinuz initrd /boot/initramfs-bootc.img options {} "#, args.kernel_args ); fs::write(&boot_entry, entry_content) .context("Failed to write boot entry")?; // Create loader configuration let loader_conf = rootfs_path.join("boot/loader/loader.conf"); let loader_content = r#"default bootc timeout 5 editor no "#; fs::write(&loader_conf, loader_content) .context("Failed to write loader configuration")?; info!("systemd-boot bootloader configured"); Ok(()) } /// Stub implementation for Unified Kernel Image (UKI) bootloader fn install_uki_bootloader(rootfs_path: &Path, _args: &Args) -> Result<()> { info!("Installing UKI bootloader (stub implementation)"); // TODO: Implement UKI bootloader support // This would involve: // 1. Creating a unified kernel image with kernel, initramfs, and bootloader // 2. Setting up proper EFI boot entries // 3. Configuring secure boot if enabled warn!("UKI bootloader support is not yet implemented - using placeholder"); // Create placeholder UKI configuration let uki_dir = rootfs_path.join("boot/uki"); fs::create_dir_all(&uki_dir) .context("Failed to create UKI directory")?; let uki_config = uki_dir.join("bootc.uki"); let config_content = r#"# UKI Configuration (stub) # This is a placeholder for future UKI support # UKI combines kernel, initramfs, and bootloader into a single EFI executable "#; fs::write(&uki_config, config_content) .context("Failed to write UKI configuration")?; info!("UKI bootloader configured (stub)"); Ok(()) } /// Stub implementation for EFI boot stub bootloader fn install_efi_boot_stub_bootloader(rootfs_path: &Path, _args: &Args) -> Result<()> { info!("Installing EFI boot stub bootloader (stub implementation)"); // TODO: Implement EFI boot stub support // This would involve: // 1. Creating a standalone EFI executable that can boot directly // 2. Embedding kernel and initramfs in the EFI binary // 3. Setting up proper EFI boot entries warn!("EFI boot stub support is not yet implemented - using placeholder"); // Create placeholder EFI boot stub configuration let efi_stub_dir = rootfs_path.join("boot/efi-stub"); fs::create_dir_all(&efi_stub_dir) .context("Failed to create EFI stub directory")?; let stub_config = efi_stub_dir.join("bootc-stub.conf"); let config_content = r#"# EFI Boot Stub Configuration (stub) # This is a placeholder for future EFI boot stub support # EFI boot stub creates a standalone EFI executable for direct booting "#; fs::write(&stub_config, config_content) .context("Failed to write EFI stub configuration")?; info!("EFI boot stub bootloader configured (stub)"); Ok(()) } /// Stub implementation for Clover bootloader fn install_clover_bootloader(rootfs_path: &Path, _args: &Args) -> Result<()> { info!("Installing Clover bootloader (stub implementation)"); // TODO: Implement Clover bootloader support // This would involve: // 1. Setting up Clover EFI bootloader configuration // 2. Creating proper ACPI tables and device properties // 3. Configuring boot entries and themes warn!("Clover bootloader support is not yet implemented - using placeholder"); // Create placeholder Clover configuration let clover_dir = rootfs_path.join("boot/clover"); fs::create_dir_all(&clover_dir) .context("Failed to create Clover directory")?; let clover_config = clover_dir.join("config.plist"); let config_content = r#" Comment Clover Configuration (stub) Description This is a placeholder for future Clover support "#; fs::write(&clover_config, config_content) .context("Failed to write Clover configuration")?; info!("Clover bootloader configured (stub)"); Ok(()) } /// Creates partitions using user's choice or fallback fn create_partitions_with_choice(image_path: &Path, choice: &str) -> Result<()> { match choice { "parted" => { info!("Using parted for partitioning"); create_partitions_with_parted(image_path) } "sgdisk" => { info!("Using sgdisk for partitioning"); create_partitions_with_sgdisk(image_path) } "sfdisk" => { info!("Using sfdisk for partitioning"); create_partitions_with_sfdisk(image_path) } "auto" => { info!("Auto-selecting partitioning tool"); create_partitions_with_fallback(image_path) } _ => { warn!("Unknown partitioning tool '{}', falling back to auto", choice); create_partitions_with_fallback(image_path) } } } /// Creates partitions using multiple tools with fallback support fn create_partitions_with_fallback(image_path: &Path) -> Result<()> { // Try different partitioning tools in order of preference let tools: Vec<(&str, fn(&Path) -> Result<()>)> = vec![ ("parted", create_partitions_with_parted), ("sgdisk", create_partitions_with_sgdisk), ("sfdisk", create_partitions_with_sfdisk), ]; let mut last_error = None; for (tool_name, partition_func) in tools { info!("Trying partitioning with {}", tool_name); match partition_func(image_path) { Ok(_) => { info!("Successfully created partitions using {}", tool_name); return Ok(()); } Err(e) => { warn!("{} failed: {}", tool_name, e); last_error = Some(e); continue; } } } Err(anyhow::anyhow!("All partitioning tools failed. Last error: {:?}", last_error)) } /// Creates partitions using parted (preferred method) fn create_partitions_with_parted(image_path: &Path) -> Result<()> { info!("Creating partitions with parted"); // Create GPT partition table let output = Command::new("/sbin/parted") .arg("-s") .arg(image_path) .arg("mklabel") .arg("gpt") .output() .context("Failed to create GPT partition table with parted")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("parted mklabel failed: {}", stderr)); } // Create single root partition (simplified for testing) let output = Command::new("/sbin/parted") .arg("-s") .arg(image_path) .arg("mkpart") .arg("primary") .arg("ext4") .arg("1MiB") .arg("100%") .output() .context("Failed to create root partition with parted")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("parted mkpart ROOT failed: {}", stderr)); } // Verify partitions were created verify_partitions(image_path)?; Ok(()) } /// Creates partitions using sgdisk (GPT-specific tool) fn create_partitions_with_sgdisk(image_path: &Path) -> Result<()> { info!("Creating partitions with sgdisk"); // Create GPT partition table and partitions in one command let output = Command::new("/usr/sbin/gdisk") .arg("--clear") .arg("--new=1:2048:104447") // EFI partition: 1MiB to 51MiB (sectors) .arg("--typecode=1:ef00") .arg("--change-name=1:EFI") .arg("--new=2:104448:0") // Root partition: 51MiB to end .arg("--typecode=2:8300") .arg("--change-name=2:ROOT") .arg(image_path) .output() .context("Failed to create partitions with sgdisk")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("sgdisk failed: {}", stderr)); } // Verify partitions were created verify_partitions(image_path)?; Ok(()) } /// Creates partitions using sfdisk (fallback method) fn create_partitions_with_sfdisk(image_path: &Path) -> Result<()> { info!("Creating partitions with sfdisk"); // Create sfdisk script - use simpler approach let sfdisk_script = r#"label: gpt start=1MiB, size=50MiB, type=ef00, name=EFI start=51MiB, size=, type=8300, name=ROOT "#; let mut child = Command::new("/usr/sbin/sfdisk") .arg(image_path) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .context("Failed to start sfdisk")?; if let Some(mut stdin) = child.stdin.take() { stdin.write_all(sfdisk_script.as_bytes()) .context("Failed to write sfdisk script")?; } let output = child.wait_with_output() .context("Failed to wait for sfdisk")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("sfdisk failed: {}", stderr)); } // Verify partitions were created verify_partitions(image_path)?; Ok(()) } /// Verifies that partitions were created successfully fn verify_partitions(image_path: &Path) -> Result<()> { info!("Verifying partitions"); // Use partprobe to detect partitions (optional) let output = Command::new("partprobe") .arg(image_path) .output(); if let Ok(output) = output { if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); warn!("partprobe failed: {}", stderr); } } else { warn!("partprobe not available, skipping partition detection"); } // List partitions to verify they exist let output = Command::new("/sbin/parted") .arg("-s") .arg(image_path) .arg("print") .output() .context("Failed to list partitions")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to list partitions: {}", stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); info!("Partition table:\n{}", stdout); // Check that we have at least 2 partitions (look for lines with partition numbers) let partition_count = stdout.lines() .filter(|line| { let trimmed = line.trim(); trimmed.starts_with(char::is_numeric) && (trimmed.contains("kB") || trimmed.contains("MB") || trimmed.contains("MiB")) }) .count(); if partition_count < 1 { return Err(anyhow::anyhow!("Expected at least 1 partition, found {}", partition_count)); } info!("Partition verification successful"); Ok(()) } /// Sets up loop device with proper error handling fn setup_loop_device(image_path: &Path) -> Result { info!("Setting up loop device for {}", image_path.display()); // Verify the image file exists and is readable if !image_path.exists() { return Err(anyhow::anyhow!("Image file does not exist: {}", image_path.display())); } let metadata = fs::metadata(image_path) .context("Failed to get image metadata")?; if metadata.len() == 0 { return Err(anyhow::anyhow!("Image file is empty: {}", image_path.display())); } info!("Image file size: {} bytes", metadata.len()); // Create loop device let losetup_output = Command::new("/usr/sbin/losetup") .arg("-f") .arg("--show") .arg(image_path) .output() .context("Failed to create loop device")?; if !losetup_output.status.success() { let stderr = String::from_utf8_lossy(&losetup_output.stderr); return Err(anyhow::anyhow!("losetup failed: {}", stderr)); } let loop_device = String::from_utf8_lossy(&losetup_output.stdout).trim().to_string(); // Verify the loop device was created if !Path::new(&loop_device).exists() { return Err(anyhow::anyhow!("Loop device was not created: {}", loop_device)); } info!("Loop device created successfully: {}", loop_device); Ok(loop_device) } /// Waits for partitions to be available and verifies they exist fn wait_for_partitions(loop_device: &str) -> Result<()> { info!("Waiting for partitions to be available"); // Run partprobe to detect partitions let output = Command::new("/usr/sbin/partprobe") .arg(loop_device) .output() .context("Failed to run partprobe")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); warn!("partprobe failed: {}", stderr); } // Wait for partitions to be available let max_attempts = 10; let mut attempts = 0; while attempts < max_attempts { std::thread::sleep(std::time::Duration::from_millis(500)); // Check if partition devices exist (single root partition) let root_partition = format!("{}p1", loop_device); if Path::new(&root_partition).exists() { info!("Partition detected: {}", root_partition); return Ok(()); } attempts += 1; info!("Waiting for partitions... attempt {}/{}", attempts, max_attempts); } Err(anyhow::anyhow!("Partitions not detected after {} attempts", max_attempts)) } /// Installs bootloader to the actual disk image (not just rootfs) fn install_bootloader_to_disk(efi_mount: &Path, root_mount: &Path, loop_device: &str, bootloader_type: &BootloaderType) -> Result<()> { match bootloader_type { BootloaderType::Grub => { info!("Installing GRUB to disk"); let output = Command::new("/usr/sbin/grub-install") .arg("--target=x86_64-efi") .arg("--efi-directory") .arg(efi_mount) .arg("--boot-directory") .arg(root_mount.join("boot")) .arg(loop_device) .output() .context("Failed to install GRUB")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); info!("Warning: GRUB installation failed: {}", stderr); } }, BootloaderType::SystemdBoot => { info!("Installing systemd-boot to disk"); // Copy systemd-boot files to EFI partition let efi_boot_dir = efi_mount.join("EFI/boot"); fs::create_dir_all(&efi_boot_dir) .context("Failed to create EFI boot directory")?; // Copy systemd-boot EFI binary (this would need to be available on the system) let bootx64_efi = efi_boot_dir.join("bootx64.efi"); let systemd_boot_efi = "/usr/lib/systemd/boot/efi/systemd-bootx64.efi"; if Path::new(systemd_boot_efi).exists() { fs::copy(systemd_boot_efi, &bootx64_efi) .context("Failed to copy systemd-boot EFI binary")?; } else { info!("Warning: systemd-boot EFI binary not found at {}", systemd_boot_efi); } // Copy boot entries to EFI partition let efi_entries_dir = efi_mount.join("loader/entries"); fs::create_dir_all(&efi_entries_dir) .context("Failed to create EFI entries directory")?; // Copy boot entries from rootfs let root_entries_dir = root_mount.join("boot/loader/entries"); if root_entries_dir.exists() { let output = Command::new("cp") .arg("-r") .arg(&root_entries_dir) .arg(efi_mount.join("loader")) .output() .context("Failed to copy boot entries")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); info!("Warning: Failed to copy boot entries: {}", stderr); } } // Create loader.conf in EFI partition let efi_loader_conf = efi_mount.join("loader/loader.conf"); let loader_content = r#"default bootc timeout 5 editor no "#; fs::write(&efi_loader_conf, loader_content) .context("Failed to write EFI loader configuration")?; }, // Future bootloader support (stub implementations) BootloaderType::UnifiedKernelImage => { info!("Installing UKI to disk (stub implementation)"); warn!("UKI disk installation is not yet implemented - using placeholder"); // TODO: Implement UKI disk installation }, BootloaderType::EfiBootStub => { info!("Installing EFI boot stub to disk (stub implementation)"); warn!("EFI boot stub disk installation is not yet implemented - using placeholder"); // TODO: Implement EFI boot stub disk installation }, BootloaderType::Clover => { info!("Installing Clover to disk (stub implementation)"); warn!("Clover disk installation is not yet implemented - using placeholder"); // TODO: Implement Clover disk installation } } Ok(()) } /// Creates a bootable disk image - SIMPLIFIED PROPER IMPLEMENTATION fn partition_and_format_disk(image_path: &Path, rootfs_path: &Path, bootloader_type: &BootloaderType, partitioner: &str, args: &Args) -> Result<()> { info!("Creating bootable disk image with proper partitioning"); // Step 1: Create raw disk image file (1GB due to space constraints) let raw_image_path = image_path.with_extension("raw.tmp"); let disk_size = args.size as u64 * 1024 * 1024 * 1024; // Size in GB converted to bytes info!("Creating raw disk image: {} ({} bytes)", raw_image_path.display(), disk_size); info!("Output image path: {}", image_path.display()); // Create sparse file using fallocate (as per your example) let output = Command::new("fallocate") .arg("-l") .arg(&disk_size.to_string()) .arg(&raw_image_path) .output() .context("Failed to create raw disk image with fallocate")?; if !output.status.success() { // Fallback to dd if fallocate fails let output = Command::new("dd") .arg("if=/dev/zero") .arg(format!("of={}", raw_image_path.display())) .arg("bs=1M") .arg(format!("count={}", disk_size / (1024 * 1024))) .output() .context("Failed to create raw disk image with dd")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to create raw disk image: {}", stderr)); } } // Step 2: Create partition table and partitions with multiple tool support info!("Creating partition table and partitions"); // Try partitioning with user's choice or fallback let partition_result = create_partitions_with_choice(&raw_image_path, partitioner); match partition_result { Ok(_) => info!("Partitions created successfully"), Err(e) => { error!("All partitioning methods failed: {}", e); return Err(anyhow::anyhow!("Failed to create partitions: {}", e)); } } // Step 3: Set up loop device and format partitions info!("Setting up loop device and formatting partitions"); let loop_device = setup_loop_device(&raw_image_path)?; info!("Using loop device: {}", loop_device); // Wait for partitions to be available and verify wait_for_partitions(&loop_device)?; // Format root partition (single partition) let root_partition = format!("{}p1", loop_device); let output = Command::new("/usr/sbin/mkfs.ext4") .arg("-F") .arg("-L") .arg("ROOT") .arg(&root_partition) .output() .context("Failed to format root partition")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("mkfs.ext4 failed: {}", stderr)); } // Step 4: Mount and copy rootfs info!("Mounting partitions and copying rootfs"); let mount_dir = Path::new("/home/joe/Projects/overwatch/tmp").join("bootc-mount"); fs::create_dir_all(&mount_dir) .context("Failed to create mount directory")?; let root_mount = mount_dir.join("root"); fs::create_dir_all(&root_mount) .context("Failed to create root mount directory")?; // Mount root partition let output = Command::new("mount") .arg(&root_partition) .arg(&root_mount) .output() .context("Failed to mount root partition")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to mount root partition: {}", stderr)); } // Copy rootfs to mounted partition let output = Command::new("rsync") .arg("-aHAX") .arg("--exclude=/dev") .arg("--exclude=/proc") .arg("--exclude=/sys") .arg("--exclude=/tmp") .arg("--exclude=*.wh.*") .arg(format!("{}/", rootfs_path.display())) .arg(format!("{}/", root_mount.display())) .output() .context("Failed to copy rootfs with rsync")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("rsync failed: {}", stderr)); } // Step 5: Install bootloader (GRUB or systemd-boot) info!("Installing bootloader"); // For single partition setup, we'll install bootloader to the root partition // Create boot directory in root partition let boot_dir = root_mount.join("boot"); fs::create_dir_all(&boot_dir) .context("Failed to create boot directory")?; // Install appropriate bootloader install_bootloader_to_disk(&boot_dir, &root_mount, &loop_device, bootloader_type)?; // Step 6: Cleanup info!("Unmounting and cleaning up"); // Unmount root partition let _ = Command::new("umount") .arg(&root_mount) .output(); // Force detach loop device let _ = Command::new("/usr/sbin/losetup") .arg("-d") .arg(&loop_device) .output(); // Additional cleanup: detach any remaining loop devices for this file let _ = Command::new("/usr/sbin/losetup") .arg("-d") .arg(&raw_image_path) .output(); // Wait for cleanup to complete std::thread::sleep(std::time::Duration::from_millis(2000)); // Remove mount directory let _ = fs::remove_dir_all(&mount_dir); // Additional wait to ensure file is ready std::thread::sleep(std::time::Duration::from_millis(1000)); // Step 7: Convert to target format info!("Converting to target format: {}", image_path.display()); // Verify the raw image exists and is ready if !raw_image_path.exists() { return Err(anyhow::anyhow!("Raw image file does not exist: {:?}", raw_image_path)); } // Check if any process is using the file let output = Command::new("lsof") .arg(&raw_image_path) .output(); if let Ok(output) = output { if output.status.success() { let stdout = String::from_utf8_lossy(&output.stdout); if !stdout.is_empty() { info!("Warning: File is still in use: {}", stdout); std::thread::sleep(std::time::Duration::from_millis(2000)); } } } // For now, just copy the raw image to the output path // TODO: Add proper format conversion info!("Copying raw image from {} to {}", raw_image_path.display(), image_path.display()); if !raw_image_path.exists() { return Err(anyhow::anyhow!("Raw image does not exist: {:?}", raw_image_path)); } fs::copy(&raw_image_path, image_path) .context("Failed to copy raw image to output path")?; // Clean up raw image let _ = fs::remove_file(&raw_image_path); // Verify final image if !image_path.exists() { return Err(anyhow::anyhow!("Final image was not created: {:?}", image_path)); } let metadata = fs::metadata(image_path).context("Failed to get final image metadata")?; info!("Bootable disk image created successfully: {:?} ({} bytes)", image_path, metadata.len()); Ok(()) } /// Converts disk image to different formats fn convert_disk_format(input_path: &Path, output_path: &Path, format: &ImageFormat) -> Result<()> { info!("Converting disk image to {:?} format", format); match format { ImageFormat::Raw => { // For raw format, just copy the file fs::copy(input_path, output_path) .context("Failed to copy raw image")?; } _ => { // For other formats, use qemu-img convert let format_str = match format { ImageFormat::Qcow2 => "qcow2", ImageFormat::Vmdk => "vmdk", ImageFormat::Iso => "iso", ImageFormat::Ami => "raw", // AMI is raw format with specific metadata _ => unreachable!(), }; if matches!(format, ImageFormat::Qcow2) { // For qcow2, we need to preserve the partition table // qemu-img convert doesn't preserve partition tables, so we use a different approach // First, create a qcow2 image with the same size as the raw image let raw_size = fs::metadata(input_path)?.len(); let mut create_cmd = Command::new("qemu-img"); create_cmd.arg("create") .arg("-f") .arg("qcow2") .arg("-o") .arg("compression_type=zstd") .arg(output_path) .arg(&format!("{}", raw_size)); let output = create_cmd.output() .context("Failed to create qcow2 image")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("qemu-img create failed: {}", stderr)); } // Then use qemu-img convert to copy the raw data into the qcow2 image // This preserves the partition table because we're not changing the disk structure let mut convert_cmd = Command::new("qemu-img"); convert_cmd.arg("convert") .arg("-f") .arg("raw") .arg("-O") .arg("qcow2") .arg("-o") .arg("compression_type=zstd") .arg(input_path) .arg(output_path); let output = convert_cmd.output() .context("Failed to convert raw to qcow2")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("qemu-img convert failed: {}", stderr)); } } else { // For other formats, use standard qemu-img convert let mut cmd = Command::new("qemu-img"); cmd.arg("convert") .arg("-f") .arg("raw") .arg("-O") .arg(format_str) .arg(input_path) .arg(output_path); let output = cmd.output() .context("Failed to convert disk image")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("qemu-img convert failed: {}", stderr)); } } } } info!("Disk image converted successfully"); Ok(()) } /// Auto-detects bootloader from container filesystem and labels fn auto_detect_bootloader(rootfs_path: &Path, image_name: &str) -> BootloaderType { // First, try filesystem detection (most reliable) if rootfs_path.join("boot/loader/entries").exists() || rootfs_path.join("usr/lib/systemd/boot").exists() { info!("Detected systemd-boot from filesystem"); return BootloaderType::SystemdBoot; } if rootfs_path.join("boot/grub/grub.cfg").exists() || rootfs_path.join("usr/lib/grub").exists() { info!("Detected GRUB from filesystem"); return BootloaderType::Grub; } // Fallback to container labels if let Ok(bootloader) = detect_bootloader_from_labels(image_name) { info!("Detected bootloader from container labels: {:?}", bootloader); return bootloader; } // Default fallback info!("Using default bootloader: GRUB"); BootloaderType::Grub } /// Detects bootloader from container image labels fn detect_bootloader_from_labels(image_name: &str) -> Result { let output = Command::new("podman") .arg("inspect") .arg(image_name) .output() .context("Failed to inspect container image")?; if output.status.success() { let inspect: serde_json::Value = serde_json::from_slice(&output.stdout) .context("Failed to parse container inspection")?; if let Some(labels) = inspect[0]["Config"]["Labels"].as_object() { if let Some(bootloader) = labels.get("bootc.bootloader") { return match bootloader.as_str() { Some("systemd-boot") => Ok(BootloaderType::SystemdBoot), _ => Ok(BootloaderType::Grub), }; } } } Err(anyhow::anyhow!("No bootloader labels found")) } /// Installs bootloader with specified type fn install_bootloader_with_type(rootfs_path: &Path, args: &Args, bootloader_type: &BootloaderType) -> Result<()> { info!("Installing bootloader: {:?}", bootloader_type); match bootloader_type { BootloaderType::Grub => install_grub_bootloader(rootfs_path, args)?, BootloaderType::SystemdBoot => install_systemd_bootloader(rootfs_path, args)?, // Future bootloader support (stub implementations) BootloaderType::UnifiedKernelImage => install_uki_bootloader(rootfs_path, args)?, BootloaderType::EfiBootStub => install_efi_boot_stub_bootloader(rootfs_path, args)?, BootloaderType::Clover => install_clover_bootloader(rootfs_path, args)?, } info!("Bootloader installed successfully"); Ok(()) } /// Tests a disk image with QEMU to verify it boots fn test_disk_image_with_qemu(image_path: &Path, timeout_seconds: u64) -> Result<()> { info!("Testing disk image with QEMU: {}", image_path.display()); // Check if QEMU is available let qemu_check = Command::new("qemu-system-x86_64") .arg("--version") .output(); if qemu_check.is_err() || !qemu_check.unwrap().status.success() { return Err(anyhow::anyhow!("QEMU is not available. Please install qemu-system-x86_64")); } // Create a temporary directory for QEMU test let temp_dir = tempdir().context("Failed to create temporary directory for QEMU test")?; let qemu_log = temp_dir.path().join("qemu.log"); info!("Starting QEMU test (timeout: {}s)", timeout_seconds); // Start QEMU with the disk image let mut qemu_process = Command::new("qemu-system-x86_64") .arg("-drive") .arg(format!("file={},format=raw", image_path.display())) .arg("-m") .arg("512") // 512MB RAM .arg("-nographic") // No graphics, serial console only .arg("-serial") .arg("stdio") .arg("-monitor") .arg("none") .arg("-no-reboot") .arg("-no-shutdown") .arg("-d") .arg("guest_errors") .arg("-D") .arg(&qemu_log) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .spawn() .context("Failed to start QEMU")?; // Monitor QEMU output for boot success indicators let start_time = std::time::Instant::now(); let mut boot_success = false; let mut boot_failure = false; // Simple timeout-based testing // In a real implementation, you would want to parse QEMU output while start_time.elapsed().as_secs() < timeout_seconds { // Check if QEMU process is still running match qemu_process.try_wait() { Ok(Some(status)) => { if status.success() { info!("QEMU exited successfully"); boot_success = true; } else { warn!("QEMU exited with error: {:?}", status); boot_failure = true; } break; } Ok(None) => { // Process still running, continue monitoring std::thread::sleep(std::time::Duration::from_millis(500)); } Err(e) => { warn!("Error checking QEMU process: {}", e); break; } } } // Terminate QEMU if it is still running if qemu_process.try_wait().unwrap_or(None).is_none() { info!("Terminating QEMU process after timeout"); let _ = qemu_process.kill(); let _ = qemu_process.wait(); } // Read QEMU log for analysis if qemu_log.exists() { let log_content = fs::read_to_string(&qemu_log) .unwrap_or_else(|_| "Could not read QEMU log".to_string()); // Look for boot success indicators if log_content.contains("bootc") || log_content.contains("systemd") || log_content.contains("init") || log_content.contains("login") { boot_success = true; } if log_content.contains("panic") || log_content.contains("error") || log_content.contains("failed") { boot_failure = true; } info!("QEMU log analysis: {} lines", log_content.lines().count()); } // Report results if boot_success { println!("✅ QEMU test PASSED - Disk image appears to boot successfully"); info!("QEMU test completed successfully"); } else if boot_failure { println!("❌ QEMU test FAILED - Disk image failed to boot properly"); return Err(anyhow::anyhow!("QEMU boot test failed")); } else { println!("⚠️ QEMU test INCONCLUSIVE - Timeout reached, boot status unclear"); warn!("QEMU test timed out after {} seconds", timeout_seconds); } Ok(()) } /// Creates a minimal kernel stub for testing purposes /// In a real implementation, this would be a proper Linux kernel fn create_kernel_stub(kernel_path: &Path) -> Result<()> { info!("Creating kernel stub at: {}", kernel_path.display()); // Create a minimal ELF executable that can be loaded by bootloaders // This is a placeholder - in reality you would copy a real kernel here let kernel_stub = b"\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00"; fs::write(kernel_path, kernel_stub) .context("Failed to create kernel stub")?; // Make it executable let mut perms = fs::metadata(kernel_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(kernel_path, perms) .context("Failed to set kernel permissions")?; info!("Kernel stub created successfully"); Ok(()) }