Files
2026-09-21 22:29:23 +08:00

100 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Create SYSTEM on ARM from a mounted prepared root, burn it, then boot the result."""
import io
import json
from pathlib import Path
import shlex
import subprocess
import sys
import tarfile
import tempfile
import time
project=Path(__file__).resolve().parents[2]
sys.path.insert(0,str(project/'tools'))
from vm_test import VM
from image_formats import digest
work=Path(tempfile.mkdtemp(prefix='m9-system.',dir=project/'out'))
# Reuse only the independently observed virtual hardware map; all images below
# are rebuilt from the current packaged rootfs. No workstation disk is exposed.
runs=sorted((project/'out').glob('m9-vm.*'),key=lambda p:p.stat().st_mtime,reverse=True)
previous=next(p for p in runs if (p/'verified-data.ext4').is_file())
with tarfile.open(previous/'burn-system.tar') as archive:
bays=archive.extractfile('etc/fds/bays.toml').read()
assert b'3 = 3' in bays,'Run the M9 source-DATA protection test first'
def build(name,rootfs):
output=work/name;output.mkdir()
with (work/f'{name}.image.log').open('wb') as log:
subprocess.run([str(project/'image/build-system-cartridge'),'--rootfs',str(rootfs),'--output-directory',str(output)],check=True,stdout=log,stderr=subprocess.STDOUT)
return output/'system.img'
source=build('prepared-source',project/'out/rootfs-cli.tar')
replacements={'etc/fds/bays.toml':bays,'usr/libexec/fds/console-session':b'#!/bin/bash\n# Isolated test image only.\nexec env HOME=/root bash --login\n'}
replacements.update({f'usr/bin/{name}':(project/'out'/name).read_bytes() for name in ['fds','fds-burn','fds-cartridged','fds-profile','fds-inspect','fds-eject']})
with tarfile.open(project/'out/rootfs-cli.tar') as src,tarfile.open(work/'creator.tar','w',format=tarfile.PAX_FORMAT) as out:
seen=set()
for member in src:
if member.name in replacements:seen.add(member.name);continue
out.addfile(member,src.extractfile(member) if member.isfile() else None)
assert seen==replacements.keys()
for path,data in replacements.items():
member=tarfile.TarInfo(path);member.size=len(data);member.mode=0o755 if path.startswith('usr/') else 0o644
out.addfile(member,io.BytesIO(data))
creator=build('creator',work/'creator.tar')
target=work/'created-system.img'
with target.open('wb') as stream:stream.truncate(640*1024*1024)
def capture(vm,command,timeout=300):
vm.send('printf "\\nM9_SYSTEM_BEGIN\\n"; '+command+'; printf "\\nM9_SYSTEM_END\\n"')
return vm.expect(rb'^M9_SYSTEM_BEGIN\r?\n(.*?)\r?\nM9_SYSTEM_END\r?$',timeout=timeout).group(1).decode().strip()
def user(vm,args):
output=capture(vm,'s6-setuidgid fds fds --json '+shlex.join(args)+' 2>/home/fds/system-command.err; printf "\\nSTATUS:%s" "$?"')
text,status=output.rsplit('STATUS:',1)
assert status.strip()=='0',(args,output,capture(vm,'cat /home/fds/system-command.err; tail -n 40 /run/log/cartridged/current'))
return json.loads(text)
with VM(work,'create',creator,system_usb=True,extra=['-m','2048','-drive',f'file={target},if=none,id=target,format=raw','-device','usb-storage,id=targetusb,drive=target,bus=xhci.0,port=2,serial=M9SYSTEM']) as vm:
vm.expect(rb'FDS# ')
capture(vm,'fds-boottrace mark console-ready; cd /home/fds')
vm.qmp('blockdev-add',{'driver':'raw','node-name':'preparedsource','read-only':True,'file':{'driver':'file','filename':str(source)}})
vm.qmp('device_add',{'driver':'usb-storage','id':'sourceusb','drive':'preparedsource','bus':'xhci.0','port':'3','serial':'M9PREPARED'})
deadline=time.monotonic()+45
while True:
bay=user(vm,['bay','3'])['bays'][0]
if bay['state']=='mounted_read_only':break
assert time.monotonic()<deadline,bay
assert bay['manifest']['cartridge']['class']=='system'
result=capture(vm,'fds-burn create system /run/fds/media/03 /home/fds/new-system.img >/home/fds/system-info.json 2>/home/fds/create-system.log; printf "CREATE_STATUS:%s" "$?"')
assert result=='CREATE_STATUS:0',(result,capture(vm,'tail -n 40 /home/fds/create-system.log'))
info=json.loads(capture(vm,'cat /home/fds/system-info.json'))
assert info['class']=='system' and info['bytes']<target.stat().st_size
(work/'created-source-info.json').write_text(json.dumps(info,indent=2)+'\n')
print('PASS: actual ARM SYSTEM construction from a complete mounted prepared root',flush=True)
prepared=user(vm,['burn','system','/home/fds/new-system.img','BAY02'])
assert prepared['image_sha256']==info['sha256']
result=user(vm,['burn','confirm',prepared['id'],prepared['confirmation']])
assert result['phase']=='complete'
(work/'write-result.json').write_text(json.dumps(result,indent=2)+'\n')
user(vm,['eject','BAY02'])
print('PASS: ordinary-user confirmed SYSTEM write, device readback and SAFE eject',flush=True)
layout=json.loads(subprocess.check_output(['sfdisk','--json',str(target)]))['partitiontable']
assert len(layout['partitions'])==1 and layout['partitions'][0]['name']=='FDS_SYSTEM'
assert layout['lastlba']==target.stat().st_size//512-34
(work/'layout.json').write_text(json.dumps(layout,indent=2)+'\n')
(work/'target.sha256').write_text(digest(target)+' created-system.img\n')
with VM(work,'boot-written-system',target,system_usb=True) as vm:
vm.expect(rb'FDS> ',timeout=120)
assert capture(vm,'id -u')=='1000'
identity=json.loads(capture(vm,'fds --json info'))
assert identity['target']=='aarch64 static-musl' and identity['pid1'].endswith('s6-svscan'),identity
assert capture(vm,'stat -c "%u:%g:%a" /usr/lib/utempter/utempter')=='0:14:2711'
assert capture(vm,'touch /etc/m9-write-rejection 2>/dev/null; printf "STATUS:%s" "$?"')=='STATUS:1'
assert capture(vm,'pgrep -x dasungd').isdigit()
assert 'Terminus' in capture(vm,'fc-match Terminus')
assert capture(vm,'test ! -d /home/fds/.cache/fontconfig; printf "CACHE_STATUS:%s" "$?"')=='CACHE_STATUS:0'
(work/'booted-identity.json').write_text(json.dumps(identity,indent=2)+'\n')
print('PASS: the SYSTEM created and written on ARM boots through stage0 to native s6 and the ordinary FDS console',flush=True)
print(f'PASS: M9 SYSTEM creation/write/boot evidence: {work}')
print('SKIP: physical Raspberry Pi boot, storage power-loss behavior and throughput')