package main import ( "fmt" "os" "os/exec" "path/filepath" "strings" ) func main() { if len(os.Args) < 2 { fmt.Println("Usage: go run create-ostree-bootable-image.go [output-path]") os.Exit(1) } rootfsPath := os.Args[1] outputPath := "debian-ostree-bootable.img" if len(os.Args) > 2 { outputPath = os.Args[2] } fmt.Printf("Creating OSTree-aware bootable image from rootfs: %s\n", rootfsPath) fmt.Printf("Output: %s\n", outputPath) // Check if rootfs exists if _, err := os.Stat(rootfsPath); os.IsNotExist(err) { fmt.Printf("Error: rootfs path does not exist: %s\n", rootfsPath) os.Exit(1) } // Check if this is an OSTree system fmt.Println("Checking for OSTree filesystem structure...") ostreeDirs := []string{ filepath.Join(rootfsPath, "usr", "lib", "ostree-boot"), filepath.Join(rootfsPath, "boot", "ostree"), filepath.Join(rootfsPath, "usr", "lib", "systemd"), filepath.Join(rootfsPath, "usr", "bin", "bootc"), filepath.Join(rootfsPath, "usr", "bin", "bootupd"), } isOstree := false for _, dir := range ostreeDirs { if _, err := os.Stat(dir); err == nil { fmt.Printf("Found OSTree component: %s\n", dir) isOstree = true } } if !isOstree { fmt.Println("Warning: This doesn't appear to be an OSTree system") fmt.Println("Falling back to standard bootable image creation...") } // Create a 5GB raw disk image imageSize := "5G" fmt.Printf("Creating %s raw disk image...\n", imageSize) // Use qemu-img to create the image cmd := exec.Command("qemu-img", "create", "-f", "raw", outputPath, imageSize) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating image: %v\n", err) os.Exit(1) } // Create a loop device fmt.Println("Setting up loop device...") cmd = exec.Command("sudo", "losetup", "--find", "--show", outputPath) output, err := cmd.Output() if err != nil { fmt.Printf("Error setting up loop device: %v\n", err) os.Exit(1) } loopDevice := strings.TrimSpace(string(output)) fmt.Printf("Loop device: %s\n", loopDevice) // Clean up loop device on exit defer func() { fmt.Printf("Cleaning up loop device: %s\n", loopDevice) exec.Command("sudo", "losetup", "-d", loopDevice).Run() }() // Create partition table (GPT) fmt.Println("Creating GPT partition table...") cmd = exec.Command("sudo", "parted", loopDevice, "mklabel", "gpt") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating partition table: %v\n", err) os.Exit(1) } // Create BIOS Boot Partition (1MB) for GRUB fmt.Println("Creating BIOS Boot Partition...") cmd = exec.Command("sudo", "parted", loopDevice, "mkpart", "primary", "1MiB", "2MiB") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating BIOS Boot Partition: %v\n", err) os.Exit(1) } // Set BIOS Boot Partition type flag fmt.Println("Setting BIOS Boot Partition type...") cmd = exec.Command("sudo", "parted", loopDevice, "set", "1", "bios_grub", "on") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not set BIOS Boot Partition type: %v\n", err) } // Create EFI System Partition (501MB) for UEFI boot fmt.Println("Creating EFI System Partition...") cmd = exec.Command("sudo", "parted", loopDevice, "mkpart", "primary", "fat32", "2MiB", "503MiB") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating EFI System Partition: %v\n", err) os.Exit(1) } // Set EFI System Partition type flag fmt.Println("Setting EFI System Partition type...") cmd = exec.Command("sudo", "parted", loopDevice, "set", "2", "esp", "on") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not set EFI System Partition type: %v\n", err) } // Create Boot Partition (1GB) for GRUB and kernel fmt.Println("Creating Boot Partition...") cmd = exec.Command("sudo", "parted", loopDevice, "mkpart", "primary", "ext4", "503MiB", "1527MiB") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating Boot Partition: %v\n", err) os.Exit(1) } // Create Root Partition (remaining space) fmt.Println("Creating Root Partition...") cmd = exec.Command("sudo", "parted", loopDevice, "mkpart", "primary", "ext4", "1527MiB", "100%") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error creating Root Partition: %v\n", err) os.Exit(1) } // Get the partition devices bootPartitionDevice := loopDevice + "p3" partitionDevice := loopDevice + "p4" fmt.Printf("Partition device: %s\n", partitionDevice) // Format EFI partition with FAT32 fmt.Printf("Formatting EFI partition with FAT32...\n") efiPartitionDevice := loopDevice + "p2" cmd = exec.Command("sudo", "mkfs.fat", "-F", "32", efiPartitionDevice) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error formatting EFI partition: %v\n", err) os.Exit(1) } // Format Boot partition with ext4 fmt.Printf("Formatting Boot partition with ext4...\n") cmd = exec.Command("sudo", "mkfs.ext4", bootPartitionDevice) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error formatting Boot partition: %v\n", err) os.Exit(1) } // Format Root partition with ext4 fmt.Printf("Formatting Root partition with ext4...\n") cmd = exec.Command("sudo", "mkfs.ext4", partitionDevice) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error formatting Root partition: %v\n", err) os.Exit(1) } // Create mount point mountPoint := "/tmp/particle-os-mount" if err := os.MkdirAll(mountPoint, 0755); err != nil { fmt.Printf("Error creating mount point: %v\n", err) os.Exit(1) } // Mount Root partition fmt.Printf("Mounting Root partition to %s...\n", mountPoint) cmd = exec.Command("sudo", "mount", partitionDevice, mountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error mounting Root partition: %v\n", err) os.Exit(1) } // Create boot directory first fmt.Printf("Creating boot directory structure...\n") bootMountPoint := filepath.Join(mountPoint, "boot") // Create boot directory and set ownership cmd = exec.Command("sudo", "mkdir", "-p", bootMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not create boot directory: %v\n", err) } // Set ownership to root cmd = exec.Command("sudo", "chown", "root:root", bootMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not set ownership: %v\n", err) } // Mount Boot partition fmt.Printf("Mounting Boot partition to %s...\n", bootMountPoint) cmd = exec.Command("sudo", "mount", bootPartitionDevice, bootMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not mount Boot partition: %v\n", err) } // Now create EFI directory inside the mounted boot partition efiMountPoint := filepath.Join(mountPoint, "boot", "efi") cmd = exec.Command("sudo", "mkdir", "-p", efiMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not create EFI directory: %v\n", err) } // Set ownership to root cmd = exec.Command("sudo", "chown", "root:root", efiMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not set ownership: %v\n", err) } // Mount EFI partition fmt.Printf("Mounting EFI partition to %s...\n", efiMountPoint) cmd = exec.Command("sudo", "mount", efiPartitionDevice, efiMountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not mount EFI partition: %v\n", err) } // Clean up mount on exit defer func() { fmt.Printf("Unmounting all partitions...\n") // Unmount in reverse order: EFI, Boot, then Root exec.Command("sudo", "umount", efiMountPoint).Run() exec.Command("sudo", "umount", bootMountPoint).Run() exec.Command("sudo", "umount", mountPoint).Run() }() // Copy rootfs content fmt.Printf("Copying rootfs content from %s to %s...\n", rootfsPath, mountPoint) cmd = exec.Command("sudo", "cp", "-a", rootfsPath+"/.", mountPoint+"/") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Error copying rootfs: %v\n", err) os.Exit(1) } // Fix permissions after copy fmt.Println("Fixing permissions...") cmd = exec.Command("sudo", "chown", "-R", "root:root", mountPoint) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not fix ownership: %v\n", err) } if isOstree { // Handle OSTree-specific setup fmt.Println("Setting up OSTree-specific boot configuration...") // Create OSTree boot directory structure ostreeBootDir := filepath.Join(mountPoint, "boot", "ostree") if err := os.MkdirAll(ostreeBootDir, 0755); err != nil { fmt.Printf("Warning: could not create ostree boot directory: %v\n", err) } // Check for kernel in OSTree location ostreeKernelDir := filepath.Join(mountPoint, "usr", "lib", "ostree-boot") if _, err := os.Stat(ostreeKernelDir); err == nil { fmt.Printf("Found OSTree kernel directory: %s\n", ostreeKernelDir) // List what's in the OSTree boot directory if files, err := os.ReadDir(ostreeKernelDir); err == nil { fmt.Println("OSTree boot contents:") for _, file := range files { fmt.Printf(" %s\n", file.Name()) } } } // Create OSTree-specific fstab fmt.Println("Creating OSTree fstab...") fstabContent := `# /etc/fstab for OSTree particle-os # Root filesystem (4th partition) /dev/sda4 / ext4 rw,errors=remount-ro 0 1 # Boot partition (3rd partition) /dev/sda3 /boot ext4 rw,errors=remount-ro 0 2 # EFI System Partition (2nd partition) /dev/sda2 /boot/efi vfat umask=0077,shortname=winnt 0 2 # BIOS Boot Partition (1st partition) - not mounted proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 devpts /dev/pts devpts gid=5,mode=620 0 0 tmpfs /run tmpfs defaults 0 0 ` fstabFile := filepath.Join(mountPoint, "etc", "fstab") cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("cat > %s << 'EOF'\n%sEOF", fstabFile, fstabContent)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not create fstab: %v\n", err) } // Try to install GRUB for OSTree fmt.Println("Installing GRUB for OSTree...") // Install GRUB using podman run to access the Debian container with GRUB tools // Mount the host's /dev directory to access loop devices // Use the actual loop device name that was created grubInstallCmd := fmt.Sprintf("grub-install --target=i386-pc --boot-directory=/mnt/boot --root-directory=/mnt %s", loopDevice) cmd = exec.Command("sudo", "podman", "run", "--rm", "--privileged", "--tty", "-v", "/dev:/dev:z", "-v", mountPoint+":/mnt:z", "238208cf481a", "bash", "-c", grubInstallCmd) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: GRUB installation failed: %v\n", err) } else { fmt.Println("GRUB installed successfully") } // Generate GRUB config using podman run grubMkconfigCmd := fmt.Sprintf("grub-mkconfig -o /mnt/boot/grub/grub.cfg") cmd = exec.Command("sudo", "podman", "run", "--rm", "--privileged", "--tty", "-v", mountPoint+":/mnt:z", "238208cf481a", "bash", "-c", grubMkconfigCmd) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: GRUB config generation failed: %v\n", err) } else { fmt.Println("GRUB config generated successfully") } // GRUB installation complete } else { // Handle standard bootable image setup fmt.Println("Setting up standard bootable image...") // Create /boot directory if it doesn't exist bootDir := filepath.Join(mountPoint, "boot") if err := os.MkdirAll(bootDir, 0755); err != nil { fmt.Printf("Warning: could not create boot directory: %v\n", err) } // Create a simple fstab fmt.Println("Creating standard fstab...") fstabContent := `# /etc/fstab for particle-os /dev/sda1 / ext4 rw,errors=remount-ro 0 1 proc /proc proc defaults 0 0 sysfs /sys sysfs defaults 0 0 devpts /dev/pts devpts gid=5,mode=620 0 0 tmpfs /run tmpfs defaults 0 0 ` fstabFile := filepath.Join(mountPoint, "etc", "fstab") cmd = exec.Command("sudo", "sh", "-c", fmt.Sprintf("cat > %s << 'EOF'\n%sEOF", fstabFile, fstabContent)) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: could not create fstab: %v\n", err) } // Install extlinux bootloader fmt.Println("Installing extlinux bootloader...") // Check if extlinux is available if _, err := exec.LookPath("extlinux"); err == nil { // Install extlinux cmd = exec.Command("sudo", "extlinux", "--install", bootDir) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { fmt.Printf("Warning: extlinux installation failed: %v\n", err) } else { fmt.Println("extlinux installed successfully") } } else { fmt.Println("extlinux not available, skipping bootloader installation") } } fmt.Printf("\n✅ OSTree-aware bootable image created successfully: %s\n", outputPath) fmt.Printf("Image size: %s\n", imageSize) fmt.Printf("Filesystem: ext4\n") if isOstree { fmt.Printf("Type: OSTree system with GRUB\n") } else { fmt.Printf("Type: Standard system with extlinux\n") } fmt.Printf("\nTo test the image:\n") fmt.Printf("qemu-system-x86_64 -m 2G -drive file=%s,format=raw -nographic -serial stdio\n", outputPath) if isOstree { fmt.Printf("\nNote: This is an OSTree system. It should boot using the OSTree boot mechanism.\n") fmt.Printf("If it doesn't boot, you may need to install a kernel in the OSTree structure.\n") } else { fmt.Printf("\nNote: This is a standard system. It should boot using the installed bootloader.\n") } }