#!/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('= 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('