Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions kernel/build-scanout.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
71 changes: 71 additions & 0 deletions kernel/package-scanout.py
Original file line number Diff line number Diff line change
@@ -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)
37 changes: 36 additions & 1 deletion kernel/scanout-slots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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/<kernel-release>/<GNU-kernel-build-id>/`. 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/<release>/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.
Loading