pack_boot.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. #!/usr/bin/env python3
  2. """
  3. ArkOS Boot Image Packager
  4. -------------------------
  5. Packs bootloader, stage2, animation, kernel and initramfs into a single boot.img.
  6. All paths are derived from --base-dir (the arkos/ directory).
  7. Usage: python3 pack_boot.py --base-dir /path/to/arkos
  8. """
  9. import argparse
  10. import os
  11. import struct
  12. import sys
  13. def align_up(val, align):
  14. return (val + align - 1) & ~(align - 1)
  15. def main():
  16. parser = argparse.ArgumentParser(description="ArkOS boot image packager")
  17. parser.add_argument("--base-dir", required=True,
  18. help="Path to the arkos/ base directory")
  19. args = parser.parse_args()
  20. base = args.base_dir
  21. bootloader_path = os.path.join(base, "out_staging", "bootloader.bin")
  22. stage2_path = os.path.join(base, "out_staging", "stage2.bin")
  23. kernel_path = os.path.join(base, "kernel", "prebuilts", "bzImage")
  24. initramfs_path = os.path.join(base, "finished", "initramfs.img")
  25. animation_path = os.path.join(base, "out_staging", "animation.bin")
  26. out_path = os.path.join(base, "finished", "boot.img")
  27. if not os.path.exists(kernel_path):
  28. kernel_path = "/boot/vmlinuz-linux-zen" # fallback
  29. with open(bootloader_path, "rb") as f:
  30. bootloader = bytearray(f.read())
  31. with open(stage2_path, "rb") as f:
  32. stage2 = bytearray(f.read())
  33. with open(kernel_path, "rb") as f:
  34. kernel = f.read()
  35. with open(initramfs_path, "rb") as f:
  36. initramfs = f.read()
  37. if os.path.exists(animation_path):
  38. with open(animation_path, "rb") as f:
  39. animation = f.read()
  40. else:
  41. animation = b""
  42. # Calculate LBA sectors (each sector is 512 bytes)
  43. bootloader_sectors = 1
  44. gpt_metadata_sectors = 33
  45. # Stage 2 MUST be exactly 7 sectors (bootloader.asm reads 7 sectors)
  46. stage2_padded_size = 7 * 512
  47. stage2 += b"\0" * (stage2_padded_size - len(stage2))
  48. animation_lba = bootloader_sectors + gpt_metadata_sectors + 7
  49. animation_size_sectors = align_up(len(animation), 512) // 512
  50. animation_padded = animation + b"\0" * (
  51. animation_size_sectors * 512 - len(animation)
  52. )
  53. bzimage_lba = animation_lba + animation_size_sectors
  54. bzimage_size_sectors = align_up(len(kernel), 512) // 512
  55. kernel_padded = kernel + b"\0" * (bzimage_size_sectors * 512 - len(kernel))
  56. initramfs_lba = bzimage_lba + bzimage_size_sectors
  57. initramfs_size_sectors = align_up(len(initramfs), 512) // 512
  58. initramfs_padded = initramfs + b"\0" * (
  59. initramfs_size_sectors * 512 - len(initramfs)
  60. )
  61. initramfs_size_bytes = len(initramfs)
  62. # Patch stage2 variables (7 consecutive dwords starting with default 8,0,0,0,0,0,0)
  63. pattern = struct.pack("<IIIIIII", 8, 0, 0, 0, 0, 0, 0)
  64. idx = stage2.find(pattern)
  65. if idx == -1:
  66. print("ERROR: Could not find variables in stage2 to patch!")
  67. sys.exit(1)
  68. print(f" Patching stage2 at offset {idx}")
  69. patched_vars = struct.pack(
  70. "<IIIIIII",
  71. bzimage_lba,
  72. bzimage_size_sectors,
  73. initramfs_lba,
  74. initramfs_size_sectors,
  75. initramfs_size_bytes,
  76. animation_lba,
  77. animation_size_sectors,
  78. )
  79. stage2[idx : idx + 28] = patched_vars
  80. # Concatenate the BIOS part to compute bios_size_bytes
  81. # We leave sectors 1-33 (16896 bytes) as zero placeholders for GPT metadata
  82. bios_data = bytearray(bootloader + b"\0" * (gpt_metadata_sectors * 512) + stage2 + animation_padded + kernel_padded + initramfs_padded)
  83. bios_size_bytes = len(bios_data)
  84. # Align bios_size_bytes to 1MB boundary (2048 sectors)
  85. bios_aligned_size = align_up(bios_size_bytes, 1024 * 1024)
  86. bios_aligned_sectors = bios_aligned_size // 512
  87. bios_padding_size = bios_aligned_size - bios_size_bytes
  88. bios_data_padded = bios_data + b"\0" * bios_padding_size
  89. # Create the EFI System Partition image (FAT32)
  90. staging_dir = os.path.join(base, "out_staging")
  91. efi_part_path = os.path.join(staging_dir, "efi_part.img")
  92. uefi_bootloader_path = os.path.join(staging_dir, "BOOTX64.EFI")
  93. print(" Creating EFI System Partition image (FAT32)...")
  94. if os.path.exists(efi_part_path):
  95. os.remove(efi_part_path)
  96. with open(efi_part_path, "wb") as f:
  97. f.write(b"\0" * (180 * 1024 * 1024))
  98. os.system(f"mkfs.vfat -F 32 -h {bios_aligned_sectors} {efi_part_path} >/dev/null 2>&1")
  99. os.system(f"mmd -i {efi_part_path} ::/EFI >/dev/null 2>&1")
  100. os.system(f"mmd -i {efi_part_path} ::/EFI/BOOT >/dev/null 2>&1")
  101. os.system(f"mcopy -i {efi_part_path} {uefi_bootloader_path} ::/EFI/BOOT/BOOTX64.EFI >/dev/null 2>&1")
  102. os.system(f"mcopy -i {efi_part_path} {kernel_path} ::/EFI/BOOT/bzImage >/dev/null 2>&1")
  103. os.system(f"mcopy -i {efi_part_path} {initramfs_path} ::/EFI/BOOT/initramfs.img >/dev/null 2>&1")
  104. os.system(f"mcopy -i {efi_part_path} {animation_path} ::/EFI/BOOT/animation.bin >/dev/null 2>&1")
  105. with open(efi_part_path, "rb") as f:
  106. efi_part_data = f.read()
  107. efi_part_sectors = len(efi_part_data) // 512
  108. # Write the hybrid image first (BIOS part + padding + UEFI partition + GPT backup padding)
  109. with open(out_path, "wb") as f:
  110. f.write(bios_data_padded)
  111. f.write(efi_part_data)
  112. f.write(b"\0" * (33 * 512))
  113. # Use sgdisk to write a valid GPT partition table on the image
  114. # Sector bios_aligned_sectors corresponds to start of EFI partition
  115. # Partition type is 'EF00' (EFI System Partition), partition name is 'EFI'
  116. import subprocess
  117. sgdisk_cmd = f"sgdisk -g -n 1:{bios_aligned_sectors}:{bios_aligned_sectors + efi_part_sectors - 1} -t 1:ef00 -c 1:'EFI' {out_path}"
  118. subprocess.run(sgdisk_cmd, shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
  119. # Overwrite the MBR boot code (first 446 bytes of Sector 0) with our bootloader
  120. with open(out_path, "r+b") as f:
  121. f.write(bootloader[:446])
  122. total = os.path.getsize(out_path)
  123. print(f" boot.img (hybrid UEFI/BIOS) built successfully! ({total:,} bytes)")
  124. if __name__ == "__main__":
  125. main()