pack_boot.py 6.6 KB

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