diff --git a/.github/workflows/ci_build.yml b/.github/workflows/ci_build.yml index c4f7b30..2ec18e5 100644 --- a/.github/workflows/ci_build.yml +++ b/.github/workflows/ci_build.yml @@ -57,8 +57,8 @@ jobs: - name: Upload scanout module uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: - name: zaparoo-scanout-6.18.38-MiSTer - path: kernel/scanout-slots/zaparoo_scanout.ko + name: zaparoo-scanout + path: kernel/.build/ci/zaparoo-scanout.zip if-no-files-found: error build: diff --git a/kernel/build-scanout.sh b/kernel/build-scanout.sh index 5a82990..c319c6e 100644 --- a/kernel/build-scanout.sh +++ b/kernel/build-scanout.sh @@ -58,3 +58,10 @@ make -C "$root/kernel/scanout-slots" KERNEL_SRC="$kernel_src" KERNEL_BUILD="$ker vermagic=$(modinfo -F vermagic "$root/kernel/scanout-slots/zaparoo_scanout.ko") test "${vermagic% }" = '6.18.38-MiSTer SMP mod_unload ARMv7 p2v8' sha256sum "$root/kernel/scanout-slots/zaparoo_scanout.ko" + +# The installed profile binds runtime selection to the built kernel, not merely +# uname -r. A stock image with a different ID remains on fb0 until qualified. +python3 "$root/kernel/package-scanout.py" \ + --kernel-build "$kernel_build" \ + --module "$root/kernel/scanout-slots/zaparoo_scanout.ko" \ + --output "$build_root/zaparoo-scanout.zip" diff --git a/kernel/package-scanout.py b/kernel/package-scanout.py new file mode 100644 index 0000000..d1f2ef2 --- /dev/null +++ b/kernel/package-scanout.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Package an exact kernel/module pair; this does not establish device qualification.""" +import argparse +import hashlib +import json +from pathlib import Path +import re +import subprocess +import zipfile + + +def digest(path): + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def build_id(path): + notes = subprocess.check_output(['readelf', '-n', str(path)], text=True) + ids = re.findall(r'Build ID: ([0-9a-f]+)', notes) + if len(ids) != 1 or not re.fullmatch(r'(?:[0-9a-f]{2}){16,64}', ids[0]): + raise ValueError(f'{path}: expected one GNU build ID') + return ids[0] + + +def package(kernel_build, module, output): + root = Path(__file__).resolve().parents[1] + source = root / 'kernel/scanout-slots' + header = (source / 'zaparoo_scanout_platform.h').read_text() + revision = re.search(r'ZAPAROO_SCANOUT_KERNEL_REVISION "([0-9a-f]{40})"', header)[1] + release = re.search(r'ZAPAROO_SCANOUT_KERNEL_RELEASE "([A-Za-z0-9._-]+)"', header)[1] + if (kernel_build / 'include/config/kernel.release').read_text().strip() != release: + raise ValueError('kernel release differs from the platform contract') + def info(field): + return subprocess.check_output(['modinfo', '-F', field, str(module)], text=True).strip() + if info('kernel_revision') != revision or info('name') != 'zaparoo_scanout': + raise ValueError('module identity differs from the platform contract') + if info('vermagic') != release + ' SMP mod_unload ARMv7 p2v8': + raise ValueError('unexpected vermagic') + kernel_id, module_id = build_id(kernel_build / 'vmlinux'), build_id(module) + profile = '\n'.join(['ZAPAROO-SCANOUT-PROFILE-1', release, kernel_id, + module_id, digest(module), revision, 'zaparoo-scanout-v1-1080p', '']) + provenance = { + 'schema': 1, 'kernel_revision': revision, + 'kernel_build_id': kernel_id, 'module_build_id': module_id, + 'kernel_config_sha256': digest(kernel_build / '.config'), + 'kernel_symvers_sha256': digest(kernel_build / 'Module.symvers'), + 'module_sha256': digest(module), + 'source_sha256': {p.name: digest(p) for p in sorted(source.iterdir()) + if p.name in ('zaparoo_scanout.c', 'zaparoo_scanout_platform.h', + 'zaparoo_scanout_uapi.h', 'Makefile')}, + 'qualification': 'Build provenance only; device qualification is required before distribution.', + } + prefix = f'modules/{release}/{kernel_id}/' + output.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as archive: + archive.writestr(prefix + 'profile', profile) + archive.write(module, prefix + 'zaparoo_scanout.ko') + archive.writestr(prefix + 'provenance.json', json.dumps(provenance, indent=2) + '\n') + # Ship the matching source and existing attribution alongside the object. + for name in provenance['source_sha256']: + archive.write(source / name, prefix + 'source/' + name) + archive.write(source / 'README.md', prefix + 'source/README.md') + print(f'{output}: kernel {kernel_id}, module {module_id}') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--kernel-build', required=True, type=Path) + parser.add_argument('--module', required=True, type=Path) + parser.add_argument('--output', required=True, type=Path) + args = parser.parse_args() + package(args.kernel_build, args.module, args.output) diff --git a/kernel/scanout-slots/README.md b/kernel/scanout-slots/README.md index 90ed93c..6bcab58 100644 --- a/kernel/scanout-slots/README.md +++ b/kernel/scanout-slots/README.md @@ -44,7 +44,7 @@ Do not claim concurrent DreamSTer/MagiK/Zaparoo rendering is supported. ## Qualified build CI builds the module in a separate job from Quartus and uploads -`zaparoo-scanout-6.18.38-MiSTer` containing `zaparoo_scanout.ko`. It does not +`zaparoo-scanout` containing `zaparoo-scanout.zip`. It does not install the module or change the device kernel. Only the checksum-verified compiler archive is cached; kernel output and `Module.symvers` are built fresh. @@ -101,3 +101,38 @@ executable mappings and fork inheritance are disabled. Successful compilation is **not hardware qualification**. Before installing or distributing the artifact, verify the matched Main/frontend/Menu stack on the target device, including ownership handoff, crash recovery and fb0 fallback. + +## Exact-build distribution profiles + +Update All's default distribution pins a reviewed Linux image; its Edge Linux +option follows the official newest image. These are moving distribution policies, +not kernel ABIs. Do not select a module from downloader settings or the image on +disk: the running kernel may still precede an update awaiting reboot. + +`kernel/package-scanout.py` runs after the qualified build and writes +`kernel/.build/ci/zaparoo-scanout.zip`. Each profile is installed under +`zaparoo/modules///`. The profile contains +the module build ID, SHA-256, kernel revision, and Zaparoo's v1 1080p contract +identifier. The ZIP also carries kernel config/symbol-table checksums and matching +module sources with their existing attribution. Do not strip or rewrite the +module after packaging. This is integrity/provenance, not a signature. + +Main reads `/sys/kernel/notes` and accepts only that exact profile; it checks the +module digest before insmod and the loaded module's GNU build ID before granting +a lease. Missing notes, missing profiles and mismatches retain fb0. Old flat +`modules//zaparoo_scanout.ko` installations no longer enable scanout. + +The existing source/config/compiler pins remain in force. A locally built kernel +with the same release string as a stock image is not evidence of compatibility: +its build ID may differ. Never relabel a profile to match a different image. +Adding a pinned or Edge build requires its exact source/config/symbol inputs, +verification against the actual running image, and the hardware tests above. +Multiple tested builds with the same release string can coexist in one bundle. + +After device qualification of the matched Main/Menu/frontend stack, attach +`zaparoo-scanout.zip` to that Menu release explicitly. The frontend packaging +script includes this asset only when `ZAPAROO_INCLUDE_SCANOUT=1`; a requested +missing or invalid bundle fails packaging. CI generation alone does not publish +or qualify the module. Keep older tested profiles available for users who switch +streams or have not rebooted yet. MagiK's larger Main-window mapping, diagnostics +and FPGA protocol changes are not imported by this identity-only update.