Support native SD storage and consolidate fds-flash tooling

Accept native SD cards for internal settings alongside legacy NVMe,
reject USB ancestry and ambiguous disks, and preserve read-only boot
loading with explicit recovery writes. Require built-in MMC drivers,
validate cached kernel configuration, and provide SD-only, SD-first and
USB-first EEPROM profiles.

Expose typed create and inspect commands through fds-flash, reuse the
existing cartridge creation code, and document the optional e2fsprogs
package dependency. Extend storage, EEPROM, flashing and hardware-test
coverage and advance the wiki reference to the published SD guide.

Validation: rootfs checks for CLI/development, boot matrix, internal
storage, EEPROM, flash, workstation, emulator, make check and wiki checks
passed. The full internal suite passed on an unchanged rerun after one
unexplained VM shutdown stall. Standalone init-test was blocked by its
unavailable pinned upstream kernel. Physical Pi checks remain pending.
This commit is contained in:
2026-09-27 00:29:53 +08:00
parent a14ea77145
commit f4bc28043a
22 changed files with 527 additions and 97 deletions
+47 -2
View File
@@ -87,6 +87,11 @@ def verify(image, disk):
for image in [internal, system]:
source_hash = digest(image)
inspection = json.loads(invoke(['inspect', image, '--json']).stdout)
assert inspection['status'] == 'inspected'
assert inspection['inspection']['sha256'] == source_hash
assert inspection['inspection']['image']['kind'] == image.stem
assert source_hash in invoke(['inspect', image]).stdout
for extra in [0, 512, 4*1024*1024]:
disk = target(f'{image.stem}-{extra}.target', image.stat().st_size + extra)
before = digest(disk)
@@ -108,11 +113,48 @@ for name, partitions in [
('software', [(label, LINUX_FILESYSTEM, parts[1][2]) for label in ('FDS_METADATA','FDS_PAYLOAD02','FDS_PAYLOAD03')]),
]:
image=work/(name+'.img');gpt(image,partitions)
inspection=json.loads(invoke(['inspect',image,'--json']).stdout)['inspection']
assert inspection['sha256']==digest(image)
assert [p['name'] for p in inspection['image']['partitions']]==[p[0] for p in partitions]
disk=target(name+'.target',image.stat().st_size+1024*1024)
plan=preview(image,disk)
assert json.loads(invoke(unattended(image,disk,plan)).stdout)['status']=='verified'
verify(image,disk)
# The consolidated command creates a real DATA filesystem, inspects it, and
# flashes it through the same unattended writer used for existing images.
# e2fsprogs is required for this acceptance check, as for DATA creation itself.
tree=work/'data-tree';(tree/'FDS').mkdir(parents=True)
(tree/'FDS/CARTRIDGE.TOML').write_text('format=1\n[cartridge]\nid="flash.data"\nname="Flash test"\nclass="data"\nversion="1"\n[media]\nwritable=true\n')
(tree/'sample.txt').write_text('Created and flashed with fds-flash\n')
created=work/'created-data.img'
result=json.loads(invoke(['create','data',tree,created,'--size-mib','32','--json']).stdout)
assert result['status']=='created'
inspection=json.loads(invoke(['inspect',created,'--json']).stdout)['inspection']
assert result['inspection']==inspection and inspection['sha256']==digest(created)
disk=target('created-data.target',created.stat().st_size+1024*1024)
assert json.loads(invoke(unattended(created,disk,preview(created,disk))).stdout)['status']=='verified'
verify(created,disk)
partition=inspection['image']['partitions'][0]
payload=work/'created-data.ext4'
with disk.open('rb') as stream:
stream.seek(partition['start']);payload.write_bytes(stream.read(partition['bytes']))
readback=subprocess.run(['debugfs','-R','cat /sample.txt',str(payload)],capture_output=True,text=True,check=True)
assert readback.stdout==(tree/'sample.txt').read_text(),readback
before=digest(created)
invoke(['create','data',tree,created,'--size-mib','32'],False)
assert digest(created)==before
for arguments in [
['create','data',tree,tree/'inside.img','--size-mib','32'],
['create','environment',tree,work/'wrong-class.img'],
['create','system',tree,work/'wrong-system.img'],
['create','program',tree,work/'legacy-program.img'],
['create','data',tree,work/'bad-size.img','--size-mib','31'],
]:
invoke(arguments,False)
assert not (tree/'inside.img').exists()
assert not any((work/name).exists() for name in ['wrong-class.img','wrong-system.img','legacy-program.img','bad-size.img'])
# Invalid source geometry and destinations must fail without writing.
disk = target('protected.target', internal.stat().st_size)
before = digest(disk)
@@ -130,7 +172,9 @@ for name, change in [('bad-primary', 512+16), ('bad-backup', internal.stat().st_
with image.open('r+b') as stream:
stream.seek(change); byte=stream.read(1); stream.seek(change); stream.write(bytes([byte[0]^1]))
invoke(['--image', image, '--device', disk, '--file-target', '--dry-run'], False)
invoke(['inspect', image, '--json'], False)
invoke(['--image', parts[0][2], '--device', disk, '--file-target', '--dry-run'], False)
invoke(['inspect', parts[0][2]], False)
assert digest(disk) == before
# Drive the real terminal workflow, including cancellation and changed inputs
@@ -198,10 +242,11 @@ for name in ('fds-internal.img', 'fds-system-cli.img'):
print('SKIP: build image not available for read-only inspection:',image)
record=dict(status='passed',cli=str(cli),cli_sha256=digest(cli),internal_and_all_cartridge_classes=True,interactive_terminal=True,
image_inspection_without_target=True,data_create_inspect_flash_and_filesystem_readback=True,
unattended_bindings=True,exact_larger_and_overlapping_gpt=True,independent_sfdisk=True,
partition_payloads_unchanged=True,invalid_sources_and_targets_rejected=True,
changed_source_and_target_rejected=True,actual_build_images_inspected=actual_images,physical_disks_written=False)
(work/'acceptance.json').write_text(json.dumps(record,indent=2)+'\n')
(project/'out/workstation-flash-current.txt').write_text(str(work)+'\n')
print('PASS: interactive/unattended flash, rejection paths, payload readback and relocated GPT:',work)
print('SKIP: physical devices; fixtures are disposable files with inert filesystem signatures')
print('PASS: create/inspect, interactive/unattended flash, rejection paths, payload readback and relocated GPT:',work)
print('SKIP: physical devices; disposable-file fixtures include real DATA and inert signatures for other layouts')