Accept native SD cards for internal settings alongside legacy NVMe, reject USB ancestry and ambiguous disks, and preserve read-only boot loading with explicit recovery writes. Require built-in MMC drivers, validate cached kernel configuration, and provide SD-only, SD-first and USB-first EEPROM profiles. Expose typed create and inspect commands through fds-flash, reuse the existing cartridge creation code, and document the optional e2fsprogs package dependency. Extend storage, EEPROM, flashing and hardware-test coverage and advance the wiki reference to the published SD guide. Validation: rootfs checks for CLI/development, boot matrix, internal storage, EEPROM, flash, workstation, emulator, make check and wiki checks passed. The full internal suite passed on an unchanged rerun after one unexplained VM shutdown stall. Standalone init-test was blocked by its unavailable pinned upstream kernel. Physical Pi checks remain pending.
103 lines
5.4 KiB
Python
Executable File
103 lines
5.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Assemble and read back a Pi 5 FAT32 boot partition; never access a host disk."""
|
|
import argparse
|
|
import datetime
|
|
import json
|
|
import mmap
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
|
|
project = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(project/'tools'))
|
|
from image_formats import digest
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--mode', choices=['production', 'development'], default='production')
|
|
args = parser.parse_args()
|
|
subprocess.run([str(project/'tools/prepare-pi-firmware')], check=True)
|
|
subprocess.run([str(project/'tools/prepare-image-tools')], check=True)
|
|
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
|
firmware_config = dict(line.split('=', 1) for line in (project/'config/pi-firmware.conf').read_text().splitlines() if line and not line.startswith('#'))
|
|
package = project/'out/cache/boot'/f'rpi-firmware-{firmware_config["PI_FIRMWARE_VERSION"]}.aarch64.xbps'
|
|
work = Path(tempfile.mkdtemp(prefix='boot-build.', dir=project/'out'))
|
|
files = work/'files'
|
|
files.mkdir()
|
|
with tarfile.open(package) as archive:
|
|
for member in archive:
|
|
name = member.name.removeprefix('./')
|
|
if not member.isfile(): continue
|
|
relative = Path(name)
|
|
if relative.parent == Path('boot') and relative.suffix in ('.elf', '.dat', '.bin'):
|
|
(files/relative.name).write_bytes(archive.extractfile(member).read())
|
|
elif relative.parent == Path('usr/share/licenses/rpi-firmware'):
|
|
(files/relative.name).write_bytes(archive.extractfile(member).read())
|
|
kernel = project/'out/kernel/boot'
|
|
shutil.copy2(kernel/'kernel_2712.img', files)
|
|
for dtb in sorted(kernel.glob('bcm2712-*.dtb')): shutil.copy2(dtb, files)
|
|
shutil.copytree(kernel/'overlays', files/'overlays')
|
|
shutil.copy2(project/'out/fds-initramfs.img', files/'fds-initramfs.img')
|
|
shutil.copy2(project/'image/pi5/config.txt', files)
|
|
shutil.copy2(project/'image/pi5'/f'cmdline-{args.mode}.txt', files/'cmdline.txt')
|
|
assert (files/'bcm2712-rpi-5-b.dtb').is_file()
|
|
assert len((files/'cmdline.txt').read_text().splitlines()) == 1
|
|
for path in sorted(files.rglob('*')): os.utime(path, (epoch, epoch))
|
|
image = work/'boot.fat'
|
|
runner = str(project/'tools/in-image-tools')
|
|
subprocess.run([runner, 'mkfs.fat', '--invariant', '-C', '-F', '32', '-n', 'FDS_BOOT', str(image), '524288'], check=True)
|
|
for path in sorted(files.iterdir()):
|
|
subprocess.run([runner, 'mcopy', '-m', '-s', '-i', str(image), str(path), '::/'], check=True)
|
|
|
|
# mtools preserves file times but creates directory records at wall-clock time.
|
|
# Normalize FAT32 directory timestamps without touching file data or long names.
|
|
stamp = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc)
|
|
date = ((stamp.year-1980)<<9) | (stamp.month<<5) | stamp.day
|
|
clock = (stamp.hour<<11) | (stamp.minute<<5) | (stamp.second//2)
|
|
with image.open('r+b') as stream, mmap.mmap(stream.fileno(), 0) as disk:
|
|
sector = struct.unpack_from('<H', disk, 11)[0]
|
|
cluster_bytes = sector*disk[13]
|
|
reserved = struct.unpack_from('<H', disk, 14)[0]
|
|
fat_sectors = struct.unpack_from('<I', disk, 36)[0]
|
|
fat_offset = reserved*sector
|
|
data_offset = (reserved+disk[16]*fat_sectors)*sector
|
|
root = struct.unpack_from('<I', disk, 44)[0]
|
|
visited = set()
|
|
def directory(cluster):
|
|
while cluster < 0x0ffffff8:
|
|
assert cluster >= 2 and cluster not in visited
|
|
visited.add(cluster)
|
|
start = data_offset+(cluster-2)*cluster_bytes
|
|
assert start+cluster_bytes <= len(disk)
|
|
for offset in range(start, start+cluster_bytes, 32):
|
|
if disk[offset] == 0: break
|
|
if disk[offset] == 0xe5 or disk[offset+11] == 0x0f: continue
|
|
disk[offset+13] = (stamp.second%2)*100
|
|
struct.pack_into('<HHH', disk, offset+14, clock, date, date)
|
|
struct.pack_into('<HH', disk, offset+22, clock, date)
|
|
if disk[offset+11] & 0x10 and disk[offset] != ord('.'):
|
|
child = (struct.unpack_from('<H', disk, offset+20)[0]<<16) | struct.unpack_from('<H', disk, offset+26)[0]
|
|
directory(child)
|
|
cluster = struct.unpack_from('<I', disk, fat_offset+cluster*4)[0] & 0x0fffffff
|
|
directory(root)
|
|
disk.flush()
|
|
subprocess.run([runner, 'fsck.fat', '-n', str(image)], check=True)
|
|
readback = work/'readback'
|
|
readback.mkdir()
|
|
subprocess.run([runner, 'mcopy', '-s', '-i', str(image), '::*', str(readback)], check=True)
|
|
expected = {str(path.relative_to(files)): digest(path) for path in sorted(files.rglob('*')) if path.is_file()}
|
|
observed = {str(path.relative_to(readback)): digest(path) for path in sorted(readback.rglob('*')) if path.is_file()}
|
|
assert observed == expected, 'FAT read-back mismatch'
|
|
(work/'manifest.json').write_text(json.dumps({'mode': args.mode, 'bytes': image.stat().st_size,
|
|
'image_sha256': digest(image), 'firmware_sha256': digest(package), 'files': expected}, indent=2)+'\n')
|
|
for name, source in [('fds-boot.img', work.name+'/boot.fat'), ('boot-volume', work.name)]:
|
|
target = project/'out'/(name+'.next')
|
|
target.symlink_to(source)
|
|
target.replace(project/'out'/name)
|
|
print(f'PASS: 512 MiB FDS_BOOT FAT32 partition image, all files verified: {work}')
|
|
print('SKIP: firmware boot, native SD and display mode validation require a physical Pi')
|