#!/usr/bin/python3 """ Helper to build Raspberry Pi images. The PiRogue image generation tool would ideally use daily auto-built images provided by Debian (https://raspi.debian.net/) but the build process might be stalled for several weeks or months, so we're building our own image(s) in a weekly fashion. This makes sure we don't end up shipping PiRogue images containing severely outdated packages (meaning a possibly significant amount of missing security/stability fixes). This script is a wrapper around image-specs's Makefile, and it also helps maintain the required associated sudo rules. Generate sudo configuration for the specified image(s): ./raspi-build --sudo raspi_4_bookworm Build, compress, checksum, and rsync the specified images(s) to a web server (see RSYNC_DEST): ./raspi-build --build raspi_4_bookworm """ import argparse import hashlib import logging import os import subprocess import sys from pathlib import Path # Settings: WORK_DIR = Path('~/image-specs').expanduser() RSYNC_DEST = 'website:pirogue.apt.debamax.com/raspi-images/' def sudo(images): """ Generate the sudo snippet for all specified images. """ print(f'# Generated by {Path(__file__).resolve()}') for image in images: # Keep in sync with the Makefile in image-specs: print(f'{login} ALL=NOPASSWD: /usr/bin/vmdb2 --verbose --rootfs-tarball={image}.tar.gz ' f'--output={image}.img {image}.yaml --log {image}.log') # Keep in sync with root-owned products/byproducts (also noting the # escaped ':' character): print(f'{login} ALL=NOPASSWD: /usr/bin/chown {uid}\\:{gid} {WORK_DIR}/{image}.img') print(f'{login} ALL=NOPASSWD: /usr/bin/chown {uid}\\:{gid} {WORK_DIR}/{image}.tar.gz') def run(title, *cmd): """ Log and exec. """ logging.info(title) subprocess.check_call(*cmd) def build(images): """ Build all specified images. """ os.chdir(WORK_DIR) for image in images: logging.info('=== BEGIN: WORK WITH %s ===', image) # Generate the uncompressed images, then fix permissions on our own: run('Generate image', ['make', f'{image}.img']) run('Fix image owner', ['sudo', 'chown', f'{uid}:{gid}', f'{WORK_DIR}/{image}.img']) run('Fix tarball owner', ['sudo', 'chown', f'{uid}:{gid}', f'{WORK_DIR}/{image}.tar.gz']) # Generate compressed image ourselves to avoid timestamp-related fun if # we were using the Makefile: run('Compress image', ['xz', '-9', f'{image}.img']) # Compute checksum ourselves (same reason as above), without using # run(): sha256sum doesn't take an output argument so we'd need # shell=True. logging.info('Generate checksum') sha256 = hashlib.file_digest(Path(f'{image}.img.xz').open('rb'), 'sha256') Path(f'{image}.img.xz.sha256').write_text(f'{sha256.hexdigest()} {image}.img.xz\n') # Sync only what we need: run('Sync compressed image and checksum', ['rsync', '-av', *Path('.').glob(f'{image}.img.xz*'), RSYNC_DEST]) logging.info('=== END: WORK WITH %s ===', image) logging.info('=== BEGIN: CLEAN-UP ===') run('Cleaning git', ['git', 'clean', '-xdf']) logging.info('=== END: CLEAN-UP ===') if __name__ == '__main__': logging.basicConfig(level=logging.INFO, datefmt='%H:%M:%S', format='%(asctime)s %(message)s') parser = argparse.ArgumentParser() parser.add_argument('images', metavar='IMAGE', type=str, nargs='+', help='an image to build (e.g. raspi_4_bookworm)') parser.add_argument('--sudo', action='store_true', help='generate the sudo snippet required to build specified images') parser.add_argument('--build', action='store_true', help='build the specified images') args = parser.parse_args() login = os.getlogin() uid = os.getuid() gid = os.getgid() if args.sudo: sudo(args.images) sys.exit(0) if args.build: build(args.images) sys.exit(0)