pack_rpi4.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  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 Raspberry Pi 4 Model B -- SD Card Image Builder
  19. ------------------------------------------------------
  20. Generates a flashable SD card image (rpi4.img) containing:
  21. - Partition 1 (FAT32, 256MB): Boot partition
  22. kernel8.img, initramfs.img, bcm2711-rpi-4-b.dtb,
  23. config.txt, cmdline.txt
  24. - Partition 2 (ext4, 2GB): System partition (frameworks, libraries)
  25. - Partition 3 (ext4, 100MB): Vendor partition (DRM, keys, mirror)
  26. The GPU firmware files (start4.elf, fixup4.dat, bootcode.bin) are NOT
  27. included -- they are already present on every Raspberry Pi 4's EEPROM
  28. and SD card from the factory.
  29. Usage:
  30. python3 pack_rpi4.py --base-dir <arkos-dir>
  31. Output:
  32. <arkos-dir>/finished/rpi4/rpi4.img
  33. """
  34. import argparse
  35. import os
  36. import subprocess
  37. import sys
  38. import struct
  39. import shutil
  40. def run(cmd, check=True):
  41. """Execute a shell command and print it."""
  42. print(f" $ {cmd}")
  43. result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
  44. if result.returncode != 0 and check:
  45. print(f" ERROR: {result.stderr.strip()}")
  46. sys.exit(1)
  47. return result
  48. def ensure_dir(path):
  49. os.makedirs(path, exist_ok=True)
  50. def file_exists(path, name):
  51. """Check that a required file exists."""
  52. if not os.path.isfile(path):
  53. print(f" ERROR: Missing required file: {path}")
  54. print(f" ({name})")
  55. return False
  56. return True
  57. def safe_copy(src, dst):
  58. """Safely copy src to dst, removing existing dst file if present."""
  59. if os.path.exists(dst):
  60. try:
  61. os.chmod(dst, 0o666)
  62. os.remove(dst)
  63. except Exception:
  64. pass
  65. shutil.copyfile(src, dst)
  66. try:
  67. os.chmod(dst, 0o666)
  68. except Exception:
  69. pass
  70. def main():
  71. parser = argparse.ArgumentParser(description="ArkOS RPi4 SD Card Image Builder")
  72. parser.add_argument("--base-dir", required=True, help="Path to arkos/ directory")
  73. args = parser.parse_args()
  74. base = os.path.abspath(args.base_dir)
  75. staging = os.path.join(base, "out_staging", "rpi4")
  76. finished = os.path.join(base, "finished", "rpi4")
  77. boot_cfg = os.path.join(base, "boot", "rpi4")
  78. ensure_dir(staging)
  79. ensure_dir(finished)
  80. print("\n RPi4 SD Card Image Builder")
  81. print(" " + "=" * 40)
  82. # ── Validate required files ──────────────────────────────────
  83. print("\n Checking required files...")
  84. kernel_img = os.path.join(base, "kernel", "prebuilts", "Image")
  85. if not os.path.isfile(kernel_img):
  86. kernel_img = os.path.join(base, "kernel", "prebuilts", "arm64")
  87. initramfs = os.path.join(finished, "initramfs.img")
  88. # If initramfs is not in finished/rpi4/, check the main finished/ dir
  89. if not os.path.isfile(initramfs):
  90. initramfs = os.path.join(base, "finished", "initramfs.img")
  91. dtb = os.path.join(base, "kernel", "arch", "arm64", "boot", "dts",
  92. "broadcom", "bcm2711-rpi-4-b.dtb")
  93. config_txt = os.path.join(boot_cfg, "config.txt")
  94. cmdline_txt = os.path.join(boot_cfg, "cmdline.txt")
  95. sys_img = os.path.join(finished, "sys.img")
  96. if not os.path.isfile(sys_img):
  97. sys_img = os.path.join(base, "finished", "sys.img")
  98. vend_img = os.path.join(finished, "vend.img")
  99. if not os.path.isfile(vend_img):
  100. vend_img = os.path.join(base, "finished", "vend.img")
  101. has_kernel = os.path.isfile(kernel_img)
  102. if not has_kernel:
  103. print(f" WARNING: ARM64 kernel image missing at {kernel_img}")
  104. print(f" Compile kernel with: make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image")
  105. print(f" and place output at kernel/prebuilts/Image")
  106. has_initramfs = os.path.isfile(initramfs)
  107. if not has_initramfs:
  108. print(f" WARNING: initramfs missing at {initramfs}")
  109. ok = file_exists(config_txt, "RPi4 config.txt") and file_exists(cmdline_txt, "RPi4 cmdline.txt")
  110. # DTB is optional if user compiles kernel separately
  111. has_dtb = os.path.isfile(dtb)
  112. if not has_dtb:
  113. # Try prebuilts directory
  114. dtb_alt = os.path.join(base, "kernel", "prebuilts", "bcm2711-rpi-4-b.dtb")
  115. if os.path.isfile(dtb_alt):
  116. dtb = dtb_alt
  117. has_dtb = True
  118. else:
  119. print(f" WARNING: Device tree blob not found at {dtb}")
  120. print(f" or {dtb_alt}")
  121. print(f" Compile DTB with: make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- dtbs")
  122. if not ok:
  123. print("\n Build failed: missing required boot configuration files.")
  124. sys.exit(1)
  125. # ── Stage GPU Firmware & Boot Files ───────────────────────────
  126. fw_dir = os.path.join(boot_cfg, "firmware")
  127. ensure_dir(fw_dir)
  128. start4 = os.path.join(fw_dir, "start4.elf")
  129. fixup4 = os.path.join(fw_dir, "fixup4.dat")
  130. # If GPU firmware files are missing, download official release from raspberrypi/firmware
  131. if not os.path.isfile(start4) or not os.path.isfile(fixup4):
  132. print(f" Fetching RPi4 GPU firmware (start4.elf, fixup4.dat)...")
  133. try:
  134. import urllib.request
  135. fw_repo = "https://raw.githubusercontent.com/raspberrypi/firmware/master/boot"
  136. if not os.path.isfile(start4):
  137. urllib.request.urlretrieve(f"{fw_repo}/start4.elf", start4)
  138. print(f" Downloaded start4.elf")
  139. if not os.path.isfile(fixup4):
  140. urllib.request.urlretrieve(f"{fw_repo}/fixup4.dat", fixup4)
  141. print(f" Downloaded fixup4.dat")
  142. except Exception as e:
  143. print(f" WARNING: Could not download GPU firmware files automatically ({e}).")
  144. print(f" Place start4.elf and fixup4.dat in boot/rpi4/firmware/")
  145. # ── Stage boot partition files ───────────────────────────────
  146. print("\n Staging boot partition files...")
  147. boot_stage = os.path.join(staging, "boot")
  148. shutil.rmtree(boot_stage, ignore_errors=True)
  149. ensure_dir(boot_stage)
  150. # Copy GPU firmware if present
  151. if os.path.isfile(start4):
  152. safe_copy(start4, os.path.join(boot_stage, "start4.elf"))
  153. print(f" Copied start4.elf")
  154. if os.path.isfile(fixup4):
  155. safe_copy(fixup4, os.path.join(boot_stage, "fixup4.dat"))
  156. print(f" Copied fixup4.dat")
  157. # Copy kernel as kernel8.img (RPi4 convention for 64-bit)
  158. if has_kernel:
  159. safe_copy(kernel_img, os.path.join(boot_stage, "kernel8.img"))
  160. print(f" Copied kernel Image -> kernel8.img")
  161. # Copy initramfs
  162. if has_initramfs:
  163. safe_copy(initramfs, os.path.join(boot_stage, "initramfs.img"))
  164. print(f" Copied initramfs.img")
  165. # Copy config.txt and cmdline.txt
  166. safe_copy(config_txt, os.path.join(boot_stage, "config.txt"))
  167. safe_copy(cmdline_txt, os.path.join(boot_stage, "cmdline.txt"))
  168. print(f" Copied config.txt, cmdline.txt")
  169. # Copy DTB if available
  170. if has_dtb:
  171. safe_copy(dtb, os.path.join(boot_stage, "bcm2711-rpi-4-b.dtb"))
  172. print(f" Copied bcm2711-rpi-4-b.dtb")
  173. # Copy overlays directory if available
  174. dtbo_dir = os.path.join(base, "kernel", "prebuilts", "rpi4_dtbofiles")
  175. if os.path.isdir(dtbo_dir):
  176. boot_overlays = os.path.join(boot_stage, "overlays")
  177. ensure_dir(boot_overlays)
  178. for f in os.listdir(dtbo_dir):
  179. if f.endswith(".dtbo") or f.endswith(".dtb"):
  180. safe_copy(os.path.join(dtbo_dir, f), os.path.join(boot_overlays, f))
  181. print(f" Copied overlays/ to boot partition")
  182. # ── Create boot partition image (FAT32, 256MB) ──────────────
  183. print("\n Creating FAT32 boot partition (256MB)...")
  184. boot_img = os.path.join(staging, "boot.img")
  185. run(f"dd if=/dev/zero of={boot_img} bs=1M count=256 2>/dev/null")
  186. run(f"mkfs.vfat -F 32 -n ARKOS_BOOT {boot_img} 2>/dev/null")
  187. # Copy files into the FAT32 image using mcopy (mtools)
  188. for fname in os.listdir(boot_stage):
  189. src = os.path.join(boot_stage, fname)
  190. if os.path.isdir(src):
  191. run(f"mcopy -s -i {boot_img} {src} ::/{fname}")
  192. else:
  193. run(f"mcopy -i {boot_img} {src} ::/{fname}")
  194. print(f" Boot partition ready ({os.path.getsize(boot_img)} bytes)")
  195. # ── Assemble final SD card image ─────────────────────────────
  196. print("\n Assembling SD card image...")
  197. # Calculate sizes
  198. boot_size_mb = 256
  199. sys_size_mb = 2048
  200. vend_size_mb = 100
  201. total_mb = boot_size_mb + sys_size_mb + vend_size_mb + 6 # +6MB for GPT headers
  202. rpi4_img = os.path.join(finished, "rpi4.img")
  203. # Create empty image
  204. run(f"dd if=/dev/zero of={rpi4_img} bs=1M count={total_mb} 2>/dev/null")
  205. # Create GPT partition table with 3 partitions
  206. # Partition 1: FAT32 boot (256MB)
  207. # Partition 2: ext4 system (2GB)
  208. # Partition 3: ext4 vendor (100MB)
  209. vend_end = boot_size_mb + sys_size_mb + vend_size_mb + 4
  210. run(f"parted -s {rpi4_img} mklabel gpt")
  211. run(f"parted -s {rpi4_img} mkpart boot fat32 4MiB {boot_size_mb + 4}MiB")
  212. run(f"parted -s {rpi4_img} mkpart system ext4 {boot_size_mb + 4}MiB {boot_size_mb + sys_size_mb + 4}MiB")
  213. run(f"parted -s {rpi4_img} mkpart vendor ext4 {boot_size_mb + sys_size_mb + 4}MiB {vend_end}MiB")
  214. run(f"parted -s {rpi4_img} set 1 boot on")
  215. # Write boot partition at offset 4MiB
  216. boot_offset = 4 * 1024 * 1024
  217. run(f"dd if={boot_img} of={rpi4_img} bs=1M seek=4 conv=notrunc 2>/dev/null")
  218. # Write system partition
  219. if os.path.isfile(sys_img):
  220. sys_offset = (boot_size_mb + 4)
  221. run(f"dd if={sys_img} of={rpi4_img} bs=1M seek={sys_offset} conv=notrunc 2>/dev/null")
  222. print(f" Wrote system partition at offset {sys_offset}MB")
  223. else:
  224. print(f" WARNING: sys.img not found, system partition will be empty")
  225. # Write vendor partition
  226. if os.path.isfile(vend_img):
  227. vend_offset = (boot_size_mb + sys_size_mb + 4)
  228. run(f"dd if={vend_img} of={rpi4_img} bs=1M seek={vend_offset} conv=notrunc 2>/dev/null")
  229. print(f" Wrote vendor partition at offset {vend_offset}MB")
  230. else:
  231. print(f" WARNING: vend.img not found, vendor partition will be empty")
  232. # ── Summary ──────────────────────────────────────────────────
  233. img_size = os.path.getsize(rpi4_img)
  234. print(f"\n SD card image built successfully!")
  235. print(f" Output: {rpi4_img}")
  236. print(f" Size: {img_size:,} bytes ({img_size // (1024*1024)} MB)")
  237. print(f"\n Flash to SD card:")
  238. print(f" sudo dd if={rpi4_img} of=/dev/sdX bs=4M status=progress")
  239. print(f" sync")
  240. print()
  241. if __name__ == "__main__":
  242. main()