FDS/OS 1.0
This commit is contained in:
Executable
+87
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prepare and verify reversible Pi 5 EEPROM configuration files; never flash hardware."""
|
||||
import argparse
|
||||
import difflib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from eeprom_inputs import PROJECT,prepare,sha256
|
||||
|
||||
MANAGED={'BOOT_ORDER','BOOT_UART','NET_INSTALL_ENABLED','NET_INSTALL_AT_POWER_ON','POWER_OFF_ON_HALT','WAIT_FOR_POWER_BUTTON'}
|
||||
|
||||
def settings(text):
|
||||
if len(text.encode())>4076 or '\0' in text:raise ValueError('EEPROM configuration must fit one 4076-byte record without NUL bytes')
|
||||
result={}
|
||||
for line in text.splitlines():
|
||||
if line.lstrip().startswith('#') or '=' not in line:continue
|
||||
key,value=line.split('=',1);key=key.strip();value=value.strip()
|
||||
if key in MANAGED:
|
||||
if key in result:raise ValueError(f'Duplicate managed setting: {key}')
|
||||
result[key]=value
|
||||
return result
|
||||
|
||||
def merge(original,profile):
|
||||
expected=settings(profile)
|
||||
if set(expected)!=MANAGED:raise ValueError('Incomplete EEPROM profile')
|
||||
retained=[]
|
||||
for line in original.splitlines():
|
||||
key=line.split('=',1)[0].strip() if '=' in line and not line.lstrip().startswith('#') else None
|
||||
if key not in MANAGED:retained.append(line)
|
||||
merged='\n'.join(retained).rstrip()+'\n\n'+profile.strip()+'\n'
|
||||
if settings(merged)!=expected:raise ValueError('Merged settings mismatch')
|
||||
return merged
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--profile',choices=['production','development'],default='production')
|
||||
parser.add_argument('--base-image',type=Path,help='Regular-file Pi 5 EEPROM image; defaults to the pinned preview firmware')
|
||||
parser.add_argument('--current-config',type=Path,help='Saved board configuration to preserve instead of the image defaults')
|
||||
parser.add_argument('--output-directory',type=Path,help='New directory; existing paths are refused')
|
||||
args=parser.parse_args()
|
||||
cache,pin=prepare()
|
||||
base=(args.base_image or cache/'pieeprom.bin').resolve(strict=True)
|
||||
if not base.is_file() or base.stat().st_size!=2*1024*1024:raise ValueError('Base must be a regular 2 MiB Pi 5 EEPROM image, not a block device')
|
||||
base_checksum=sha256(base)
|
||||
tool=[sys.executable,str(cache/'rpi-eeprom-config')]
|
||||
original=subprocess.check_output([*tool,str(base)]).decode('utf-8')
|
||||
if args.current_config:
|
||||
if not args.current_config.is_file() or args.current_config.stat().st_size>4076:raise ValueError('Current configuration must be a regular file of at most 4076 bytes')
|
||||
original=args.current_config.read_text()
|
||||
# Validate bounds but preserve existing duplicates/conditional overrides in
|
||||
# the rollback configuration. All managed occurrences are replaced below.
|
||||
if len(original.encode())>4076 or '\0' in original:raise ValueError('Invalid original configuration size or NUL byte')
|
||||
profile=(PROJECT/'config/eeprom'/f'{args.profile}.conf').read_text()
|
||||
configured=merge(original,profile)
|
||||
if args.output_directory:
|
||||
work=args.output_directory.absolute();work.mkdir(mode=0o700)
|
||||
else:work=Path(tempfile.mkdtemp(prefix=f'eeprom-{args.profile}.',dir=PROJECT/'out'))
|
||||
shutil.copyfile(base,work/'base.bin')
|
||||
if sha256(work/'base.bin')!=base_checksum or sha256(base)!=base_checksum:raise ValueError('Base image changed during preparation')
|
||||
shutil.copyfile(cache/'LICENSE',work/'LICENSE')
|
||||
(work/'original.conf').write_text(original)
|
||||
(work/'configured.conf').write_text(configured)
|
||||
subprocess.run([*tool,'--config',str(work/'original.conf'),'--out',str(work/'rollback.bin'),str(work/'base.bin')],check=True)
|
||||
subprocess.run([*tool,'--config',str(work/'configured.conf'),'--out',str(work/'configured.bin'),str(work/'base.bin')],check=True)
|
||||
readback=subprocess.check_output([*tool,str(work/'configured.bin')]).decode()
|
||||
rollback=subprocess.check_output([*tool,str(work/'rollback.bin')]).decode()
|
||||
if readback!=configured or rollback!=original:raise ValueError('EEPROM configuration readback mismatch')
|
||||
(work/'review.diff').write_text(''.join(difflib.unified_diff(original.splitlines(True),configured.splitlines(True),fromfile='original.conf',tofile='configured.conf')))
|
||||
manifest={'format':1,'profile':args.profile,'upstream_commit':pin['commit'],
|
||||
'hardware_modified':False,'custom_inputs_provided':bool(args.current_config or args.base_image),
|
||||
'input_base_sha256':base_checksum,'settings':settings(configured),
|
||||
'files':{p.name:sha256(p) for p in sorted(work.iterdir()) if p.is_file()}}
|
||||
(work/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
if not args.output_directory and not args.current_config and not args.base_image:
|
||||
link=PROJECT/'out'/f'eeprom-{args.profile}-latest.next'
|
||||
link.symlink_to(work.name)
|
||||
link.replace(PROJECT/'out'/f'eeprom-{args.profile}-latest')
|
||||
print(f'Prepared and read-back verified: {work}')
|
||||
print('No hardware was modified. Review review.diff and retain base.bin, original.conf and rollback.bin.')
|
||||
if not args.current_config and not args.base_image:print('This uses pinned preview defaults; it is not a backup of your Pi.')
|
||||
|
||||
if __name__=='__main__':
|
||||
try:main()
|
||||
except (OSError,ValueError,subprocess.CalledProcessError) as error:sys.exit(f'ERROR: {error}')
|
||||
Reference in New Issue
Block a user