FDS/OS 1.0
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
# Mandatory hardware additions to every FDS SYSTEM image, including CLI-only.
|
||||
# The rootfs builder consumes this alongside the fds-base metapackage.
|
||||
# fds-base also depends on this controller so it cannot be accidentally omitted.
|
||||
fds-base
|
||||
fds-dasungd
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Assemble and read back a Pi 5 FAT32 boot partition; never access a host disk."""
|
||||
import argparse
|
||||
import datetime
|
||||
import json
|
||||
import mmap
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project/'tools'))
|
||||
from image_formats import digest
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--mode', choices=['production', 'development'], default='production')
|
||||
args = parser.parse_args()
|
||||
subprocess.run([str(project/'tools/prepare-pi-firmware')], check=True)
|
||||
subprocess.run([str(project/'tools/prepare-image-tools')], check=True)
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
firmware_config = dict(line.split('=', 1) for line in (project/'config/pi-firmware.conf').read_text().splitlines() if line and not line.startswith('#'))
|
||||
package = project/'out/cache/boot'/f'rpi-firmware-{firmware_config["PI_FIRMWARE_VERSION"]}.aarch64.xbps'
|
||||
work = Path(tempfile.mkdtemp(prefix='boot-build.', dir=project/'out'))
|
||||
files = work/'files'
|
||||
files.mkdir()
|
||||
with tarfile.open(package) as archive:
|
||||
for member in archive:
|
||||
name = member.name.removeprefix('./')
|
||||
if not member.isfile(): continue
|
||||
relative = Path(name)
|
||||
if relative.parent == Path('boot') and relative.suffix in ('.elf', '.dat', '.bin'):
|
||||
(files/relative.name).write_bytes(archive.extractfile(member).read())
|
||||
elif relative.parent == Path('usr/share/licenses/rpi-firmware'):
|
||||
(files/relative.name).write_bytes(archive.extractfile(member).read())
|
||||
kernel = project/'out/kernel/boot'
|
||||
shutil.copy2(kernel/'kernel_2712.img', files)
|
||||
for dtb in sorted(kernel.glob('bcm2712-*.dtb')): shutil.copy2(dtb, files)
|
||||
shutil.copytree(kernel/'overlays', files/'overlays')
|
||||
shutil.copy2(project/'out/fds-initramfs.img', files/'fds-initramfs.img')
|
||||
shutil.copy2(project/'image/pi5/config.txt', files)
|
||||
shutil.copy2(project/'image/pi5'/f'cmdline-{args.mode}.txt', files/'cmdline.txt')
|
||||
assert (files/'bcm2712-rpi-5-b.dtb').is_file()
|
||||
assert len((files/'cmdline.txt').read_text().splitlines()) == 1
|
||||
for path in sorted(files.rglob('*')): os.utime(path, (epoch, epoch))
|
||||
image = work/'boot.fat'
|
||||
runner = str(project/'tools/in-image-tools')
|
||||
subprocess.run([runner, 'mkfs.fat', '--invariant', '-C', '-F', '32', '-n', 'FDS_BOOT', str(image), '524288'], check=True)
|
||||
for path in sorted(files.iterdir()):
|
||||
subprocess.run([runner, 'mcopy', '-m', '-s', '-i', str(image), str(path), '::/'], check=True)
|
||||
|
||||
# mtools preserves file times but creates directory records at wall-clock time.
|
||||
# Normalize FAT32 directory timestamps without touching file data or long names.
|
||||
stamp = datetime.datetime.fromtimestamp(epoch, datetime.timezone.utc)
|
||||
date = ((stamp.year-1980)<<9) | (stamp.month<<5) | stamp.day
|
||||
clock = (stamp.hour<<11) | (stamp.minute<<5) | (stamp.second//2)
|
||||
with image.open('r+b') as stream, mmap.mmap(stream.fileno(), 0) as disk:
|
||||
sector = struct.unpack_from('<H', disk, 11)[0]
|
||||
cluster_bytes = sector*disk[13]
|
||||
reserved = struct.unpack_from('<H', disk, 14)[0]
|
||||
fat_sectors = struct.unpack_from('<I', disk, 36)[0]
|
||||
fat_offset = reserved*sector
|
||||
data_offset = (reserved+disk[16]*fat_sectors)*sector
|
||||
root = struct.unpack_from('<I', disk, 44)[0]
|
||||
visited = set()
|
||||
def directory(cluster):
|
||||
while cluster < 0x0ffffff8:
|
||||
assert cluster >= 2 and cluster not in visited
|
||||
visited.add(cluster)
|
||||
start = data_offset+(cluster-2)*cluster_bytes
|
||||
assert start+cluster_bytes <= len(disk)
|
||||
for offset in range(start, start+cluster_bytes, 32):
|
||||
if disk[offset] == 0: break
|
||||
if disk[offset] == 0xe5 or disk[offset+11] == 0x0f: continue
|
||||
disk[offset+13] = (stamp.second%2)*100
|
||||
struct.pack_into('<HHH', disk, offset+14, clock, date, date)
|
||||
struct.pack_into('<HH', disk, offset+22, clock, date)
|
||||
if disk[offset+11] & 0x10 and disk[offset] != ord('.'):
|
||||
child = (struct.unpack_from('<H', disk, offset+20)[0]<<16) | struct.unpack_from('<H', disk, offset+26)[0]
|
||||
directory(child)
|
||||
cluster = struct.unpack_from('<I', disk, fat_offset+cluster*4)[0] & 0x0fffffff
|
||||
directory(root)
|
||||
disk.flush()
|
||||
subprocess.run([runner, 'fsck.fat', '-n', str(image)], check=True)
|
||||
readback = work/'readback'
|
||||
readback.mkdir()
|
||||
subprocess.run([runner, 'mcopy', '-s', '-i', str(image), '::*', str(readback)], check=True)
|
||||
expected = {str(path.relative_to(files)): digest(path) for path in sorted(files.rglob('*')) if path.is_file()}
|
||||
observed = {str(path.relative_to(readback)): digest(path) for path in sorted(readback.rglob('*')) if path.is_file()}
|
||||
assert observed == expected, 'FAT read-back mismatch'
|
||||
(work/'manifest.json').write_text(json.dumps({'mode': args.mode, 'bytes': image.stat().st_size,
|
||||
'image_sha256': digest(image), 'firmware_sha256': digest(package), 'files': expected}, indent=2)+'\n')
|
||||
for name, source in [('fds-boot.img', work.name+'/boot.fat'), ('boot-volume', work.name)]:
|
||||
target = project/'out'/(name+'.next')
|
||||
target.symlink_to(source)
|
||||
target.replace(project/'out'/name)
|
||||
print(f'PASS: 512 MiB FDS_BOOT FAT32 partition image, all files verified: {work}')
|
||||
print('SKIP: firmware boot, NVMe and display mode validation require a physical Pi')
|
||||
Executable
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a deterministic Rust-only initramfs with the required early display daemon."""
|
||||
import argparse
|
||||
import gzip
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project/'tools'))
|
||||
from image_formats import digest, newc
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--stage0', type=Path, default=project/'out/fds-stage0')
|
||||
parser.add_argument('--output-directory', type=Path)
|
||||
args = parser.parse_args()
|
||||
payloads = {'fds-stage0': args.stage0.resolve(strict=True), 'dasungd': project/'out/dasungd'}
|
||||
|
||||
for tool in ('lz4', 'zstd'):
|
||||
if not shutil.which(tool): sys.exit(f'ERROR: missing host {tool}; see docs/m4-work.md')
|
||||
for executable in ('fds-stage0', 'dasungd'):
|
||||
subprocess.run([str(project/'tools/verify-elf'), str(payloads[executable]), 'aarch64', 'static'], check=True)
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='initramfs-build.', dir=project/'out'))
|
||||
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
|
||||
entries = []
|
||||
for name in ['dev', 'proc', 'sys', 'newroot', 'sbin', 'etc', 'usr', 'usr/lib', 'usr/lib/firmware', 'usr/lib/firmware/edid']:
|
||||
entries.append((name, stat.S_IFDIR | 0o755, b'', 0, 0))
|
||||
entries.append(('dev/console', stat.S_IFCHR | 0o600, b'', 5, 1))
|
||||
entries.append(('init', stat.S_IFLNK | 0o777, b'sbin/fds-stage0', 0, 0))
|
||||
entries.append(('lib', stat.S_IFLNK | 0o777, b'usr/lib', 0, 0))
|
||||
for executable in ('fds-stage0', 'dasungd'):
|
||||
entries.append(('sbin/'+executable, stat.S_IFREG | 0o755, payloads[executable].read_bytes(), 0, 0))
|
||||
config = (project/'packages/fds-dasungd/files/dasungd.toml').read_text().replace('/run/dasungd/', '/dev/fds-early/')
|
||||
entries.append(('etc/dasungd-early.toml', stat.S_IFREG | 0o600, config.encode(), 0, 0))
|
||||
entries.append(('usr/lib/firmware/edid/dasung-paperlike13k-37hz.bin', stat.S_IFREG | 0o644,
|
||||
(project/'rust/dasungd/profiles/paperlike13k-37hz.edid').read_bytes(), 0, 0))
|
||||
archive = newc(sorted(entries), epoch)
|
||||
(work/'initramfs.cpio').write_bytes(archive)
|
||||
(work/'initramfs.cpio.gz').write_bytes(gzip.compress(archive, compresslevel=9, mtime=0))
|
||||
for tool, options, suffix in [('lz4', ['-q', '-l', '-9', '-c'], 'lz4'), ('zstd', ['-q', '-T1', '-19', '-c'], 'zst')]:
|
||||
with (work/f'initramfs.cpio.{suffix}').open('wb') as stream:
|
||||
subprocess.run([tool, *options], input=archive, stdout=stream, check=True)
|
||||
# All variants must decode to the exact same cpio stream.
|
||||
assert gzip.decompress((work/'initramfs.cpio.gz').read_bytes()) == archive
|
||||
for tool, suffix in [('lz4', 'lz4'), ('zstd', 'zst')]:
|
||||
assert subprocess.check_output([tool, '-q', '-d', '-c', str(work/f'initramfs.cpio.{suffix}')]) == archive
|
||||
manifest = {path.name: {'bytes': path.stat().st_size, 'sha256': digest(path)} for path in sorted(work.glob('initramfs.cpio*'))}
|
||||
(work/'formats.json').write_text(json.dumps(manifest, indent=2)+'\n')
|
||||
for target, source in ([] if args.output_directory else [('initramfs', work.name), ('fds-initramfs.img', work.name+'/initramfs.cpio')]):
|
||||
temporary = project/'out'/(target+'.next')
|
||||
temporary.symlink_to(source)
|
||||
temporary.replace(project/'out'/target)
|
||||
print('PASS: uncompressed, gzip, legacy-LZ4 and Zstandard initramfs variants agree')
|
||||
print(f'Initramfs artifacts: {work}')
|
||||
print('SKIP: compression boot-time comparison requires the physical Pi')
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create a complete internal NVMe GPT image as a new ordinary file."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project / 'tools'))
|
||||
from image_formats import digest, gpt, NAMESPACE, EFI_SYSTEM, LINUX_FILESYSTEM
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--boot', type=Path, default=project / 'out/fds-boot.img')
|
||||
parser.add_argument('--recovery', type=Path, default=project / 'out/fds-recovery.img')
|
||||
parser.add_argument('--machine-config', type=Path, default=project / 'config/machine')
|
||||
parser.add_argument('--internal-mib', type=int, default=256)
|
||||
parser.add_argument('--recovery-mib', type=int, choices=[1024, 2048], default=1024)
|
||||
parser.add_argument('--output-directory', type=Path)
|
||||
args = parser.parse_args()
|
||||
if not 32 <= args.internal_mib <= 65536:
|
||||
parser.error('internal settings partition must be 32..65536 MiB')
|
||||
boot, recovery = args.boot.resolve(strict=True), args.recovery.resolve(strict=True)
|
||||
if not boot.is_file() or boot.stat().st_size != 512 * 1024 * 1024:
|
||||
parser.error('boot input must be a 512 MiB regular FAT32 partition image')
|
||||
if not recovery.is_file() or not 4096 <= recovery.stat().st_size <= args.recovery_mib * 1024 * 1024:
|
||||
parser.error('recovery input must be a regular EROFS image fitting its partition')
|
||||
with boot.open('rb') as stream:
|
||||
header = stream.read(512)
|
||||
if header[82:90] != b'FAT32 ' or header[71:82] != b'FDS_BOOT ' or header[510:] != b'\x55\xaa':
|
||||
parser.error('boot input is not FDS_BOOT FAT32')
|
||||
with recovery.open('rb') as stream:
|
||||
stream.seek(1024)
|
||||
header = stream.read(128)
|
||||
if header[:4] != bytes.fromhex('e2e1f5e0') or header[64:80].rstrip(b'\0') != b'FDS_RECOVERY':
|
||||
parser.error('recovery input is not FDS_RECOVERY EROFS')
|
||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='internal-build.', dir=project / 'out'))
|
||||
work = work.resolve(strict=True)
|
||||
if not work.is_dir() or any(work.iterdir()):
|
||||
parser.error('output directory must exist and be empty')
|
||||
subprocess.run([str(project / 'tools/cargo-build'), '--locked', '--offline', '--release', '--target', 'x86_64-unknown-linux-gnu', '-p', 'fds-cli'], cwd=project, check=True)
|
||||
host_cli = project / 'target/x86_64-unknown-linux-gnu/release/fds'
|
||||
tree = work / 'settings'
|
||||
(tree / 'config').mkdir(parents=True)
|
||||
(tree / 'diagnostics').mkdir(mode=0o700)
|
||||
subprocess.run([str(host_cli), 'machine', 'pack', str(args.machine_config.resolve(strict=True)), str(tree / 'config/machine.json')], check=True)
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project / 'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
for path in [tree, *sorted(tree.rglob('*'))]:
|
||||
os.utime(path, (epoch, epoch))
|
||||
runner = str(project / 'tools/in-image-tools')
|
||||
subprocess.run([str(project / 'tools/prepare-image-tools')], check=True)
|
||||
subprocess.run([runner, 'fsck.fat', '-n', str(boot)], check=True)
|
||||
subprocess.run([runner, 'fsck.erofs', '--extract', str(recovery)], check=True)
|
||||
settings = work / 'internal.ext4'
|
||||
with settings.open('xb') as stream:
|
||||
stream.truncate(args.internal_mib * 1024 * 1024)
|
||||
identity = uuid.uuid5(NAMESPACE, 'internal:' + digest(tree / 'config/machine.json'))
|
||||
# Source ownership is root inside this unprivileged user namespace, independent
|
||||
# of the workstation UID. No workstation block devices are exposed.
|
||||
environment = {**os.environ, 'E2FSPROGS_FAKE_TIME': str(epoch)}
|
||||
subprocess.run(['bwrap', '--unshare-user', '--uid', '0', '--gid', '0', '--ro-bind', '/', '/',
|
||||
'--bind', str(work), str(work), '--dev', '/dev', '--proc', '/proc',
|
||||
runner, 'mke2fs', '-q', '-t', 'ext4', '-F', '-b', '4096', '-m', '0',
|
||||
'-L', 'FDS_INTERNAL', '-U', str(identity),
|
||||
'-E', f'root_owner=0:0,lazy_itable_init=0,lazy_journal_init=0,hash_seed={identity}',
|
||||
'-d', str(tree), str(settings)], env=environment, check=True)
|
||||
# mke2fs copies source ctimes; normalize every populated inode after construction.
|
||||
commands = work / 'normalize.debugfs'
|
||||
commands.write_text(''.join(f'set_inode_field {name} {field} @{epoch}\n'
|
||||
for name in ['/', '/lost+found', '/config', '/config/machine.json', '/diagnostics']
|
||||
for field in ['atime', 'ctime', 'mtime', 'crtime']))
|
||||
subprocess.run([runner, 'debugfs', '-w', '-f', str(commands), str(settings)], env=environment, check=True)
|
||||
subprocess.run([runner, 'e2fsck', '-f', '-n', str(settings)], env=environment, check=True)
|
||||
padded = work / 'recovery-partition.erofs'
|
||||
shutil.copyfile(recovery, padded)
|
||||
with padded.open('r+b') as stream:
|
||||
stream.truncate(args.recovery_mib * 1024 * 1024)
|
||||
image = work / 'internal.img'
|
||||
layout = gpt(image, [('FDS_BOOT', EFI_SYSTEM, boot), ('FDS_RECOVERY', LINUX_FILESYSTEM, padded), ('FDS_INTERNAL', LINUX_FILESYSTEM, settings)])
|
||||
layout.update(format=1, image_sha256=digest(image), machine_settings_sha256=digest(tree / 'config/machine.json'),
|
||||
recovery_payload_sha256=digest(recovery), boot_payload_sha256=digest(boot))
|
||||
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(image)]))['partitiontable']
|
||||
assert observed['label'] == 'gpt' and len(observed['partitions']) == 3
|
||||
for expected, actual in zip(layout['partitions'], observed['partitions']):
|
||||
assert (actual['name'], actual['start'], actual['size']) == (expected['name'], expected['start'], expected['size'])
|
||||
subprocess.run(['sfdisk', '--verify', str(image)], check=True)
|
||||
(work / 'layout.json').write_text(json.dumps(layout, indent=2) + '\n')
|
||||
if args.output_directory is None:
|
||||
link = project / 'out/fds-internal.img.next'
|
||||
link.symlink_to(work.name + '/internal.img')
|
||||
link.replace(project / 'out/fds-internal.img')
|
||||
print(f'PASS: complete internal NVMe image, three verified GPT payloads: {work}')
|
||||
print('SKIP: no physical disk was written; Pi firmware/NVMe boot requires hardware')
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build an independent, read-only recovery filesystem from its own rootfs."""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project / 'tools'))
|
||||
from image_formats import digest, NAMESPACE
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--rootfs', type=Path, default=project / 'out/rootfs-recovery.tar')
|
||||
parser.add_argument('--output-directory', type=Path)
|
||||
args = parser.parse_args()
|
||||
rootfs = args.rootfs.resolve(strict=True)
|
||||
with tarfile.open(rootfs) as archive:
|
||||
profile = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
|
||||
try:
|
||||
root = archive.getmember('.')
|
||||
except KeyError:
|
||||
parser.error('rootfs omits root directory metadata; rebuild the rootfs')
|
||||
if not root.isdir() or (root.uid, root.gid, root.mode) != (0, 0, 0o755):
|
||||
parser.error('rootfs must explicitly record / as root:root mode 0755; rebuild the rootfs')
|
||||
if profile != 'recovery':
|
||||
parser.error('input must be a separately built recovery rootfs')
|
||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='recovery-build.', dir=project / 'out'))
|
||||
if not work.is_dir() or any(work.iterdir()):
|
||||
parser.error('output directory must exist and be empty')
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project / 'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
identity = uuid.uuid5(NAMESPACE, 'recovery:' + digest(rootfs))
|
||||
runner = str(project / 'tools/in-image-tools')
|
||||
image = work / 'recovery.erofs'
|
||||
subprocess.run([runner, 'mkfs.erofs', '-T', str(epoch), '-U', str(identity), '-b4096',
|
||||
'-L', 'FDS_RECOVERY', '--tar=f', str(image), str(rootfs)], check=True)
|
||||
subprocess.run([runner, 'fsck.erofs', '--extract', str(image)], check=True)
|
||||
(work / 'manifest.json').write_text(json.dumps({
|
||||
'format': 1, 'profile': profile, 'rootfs_sha256': digest(rootfs),
|
||||
'image_sha256': digest(image), 'bytes': image.stat().st_size,
|
||||
'partition_name': 'FDS_RECOVERY', 'filesystem': 'erofs',
|
||||
}, indent=2) + '\n')
|
||||
if args.output_directory is None:
|
||||
target = project / 'out/fds-recovery.img.next'
|
||||
target.symlink_to(work.name + '/recovery.erofs')
|
||||
target.replace(project / 'out/fds-recovery.img')
|
||||
print(f'PASS: independent recovery EROFS (partition payload): {work}')
|
||||
Executable
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Create read-only EROFS and a GPT SYSTEM image from an exported rootfs."""
|
||||
import argparse
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
project = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(project/'tools'))
|
||||
from image_formats import digest, gpt, LINUX_FILESYSTEM, NAMESPACE
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--rootfs', type=Path, default=project/'out/rootfs-aarch64.tar')
|
||||
parser.add_argument('--profile', choices=['cli', 'development'], default=None)
|
||||
parser.add_argument('--output-directory', type=Path)
|
||||
args = parser.parse_args()
|
||||
rootfs = args.rootfs.resolve(strict=True)
|
||||
with tarfile.open(rootfs) as archive:
|
||||
embedded = archive.extractfile('usr/share/fds/image-profile').read(64).decode().strip()
|
||||
try:
|
||||
root = archive.getmember('.')
|
||||
except KeyError:
|
||||
parser.error('rootfs omits root directory metadata; rebuild the rootfs')
|
||||
if not root.isdir() or (root.uid, root.gid, root.mode) != (0, 0, 0o755):
|
||||
parser.error('rootfs must explicitly record / as root:root mode 0755; rebuild the rootfs')
|
||||
if embedded not in ('cli', 'development'): parser.error('unsupported rootfs profile')
|
||||
if args.profile is not None and args.profile != embedded: parser.error('requested profile does not match rootfs contents')
|
||||
args.profile = embedded
|
||||
work = args.output_directory or Path(tempfile.mkdtemp(prefix='system-build.', dir=project/'out'))
|
||||
if not work.is_dir() or any(work.iterdir()): parser.error('output directory must exist and be empty')
|
||||
epoch = int(subprocess.check_output(['git', '-C', str(project/'vendor/void-packages'), 'show', '-s', '--format=%ct', 'HEAD']))
|
||||
manifest = f'''format = 1\n[cartridge]\nid = "fds.system.{args.profile}"\nname = "FDS/OS {args.profile.upper()}"\nclass = "system"\nversion = "0.1.0"\n[media]\nwritable = false\n'''.encode()
|
||||
with tarfile.open(rootfs) as source, tarfile.open(work/'system.tar', 'w', format=tarfile.PAX_FORMAT) as output:
|
||||
for member in source:
|
||||
if member.name in ('FDS', 'FDS/CARTRIDGE.TOML'): continue
|
||||
output.addfile(member, source.extractfile(member) if member.isfile() else None)
|
||||
directory = tarfile.TarInfo('FDS')
|
||||
directory.type = tarfile.DIRTYPE
|
||||
directory.mode = 0o755
|
||||
directory.mtime = epoch
|
||||
output.addfile(directory)
|
||||
info = tarfile.TarInfo('FDS/CARTRIDGE.TOML')
|
||||
info.mode = 0o644
|
||||
info.mtime = epoch
|
||||
info.size = len(manifest)
|
||||
output.addfile(info, io.BytesIO(manifest))
|
||||
image_uuid = uuid.uuid5(NAMESPACE, digest(work/'system.tar'))
|
||||
subprocess.run([str(project/'tools/in-image-tools'), 'mkfs.erofs', '-T', str(epoch), '-U', str(image_uuid),
|
||||
'-b4096', '-L', 'FDS_SYSTEM', '--tar=f', str(work/'system.erofs'), str(work/'system.tar')], check=True)
|
||||
subprocess.run([str(project/'tools/in-image-tools'), 'fsck.erofs', '--extract', str(work/'system.erofs')], check=True)
|
||||
layout = gpt(work/'system.img', [('FDS_SYSTEM', LINUX_FILESYSTEM, work/'system.erofs')])
|
||||
layout['rootfs_sha256'] = digest(rootfs)
|
||||
layout['image_sha256'] = digest(work/'system.img')
|
||||
layout['profile'] = args.profile
|
||||
(work/'layout.json').write_text(json.dumps(layout, indent=2)+'\n')
|
||||
# An independent implementation checks the actual on-disk partition table.
|
||||
observed = json.loads(subprocess.check_output(['sfdisk', '--json', str(work/'system.img')]))['partitiontable']
|
||||
assert observed['label'] == 'gpt' and observed['partitions'][0]['name'] == 'FDS_SYSTEM'
|
||||
assert observed['partitions'][0]['start'] == layout['partitions'][0]['start']
|
||||
if args.output_directory is None:
|
||||
target = project/'out'/f'fds-system-{args.profile}.img'
|
||||
temporary = target.with_suffix('.img.next')
|
||||
temporary.symlink_to(work.name+'/system.img')
|
||||
temporary.replace(target)
|
||||
print(f'PASS: GPT FDS_SYSTEM and verified EROFS: {work}')
|
||||
@@ -0,0 +1,6 @@
|
||||
# Mandatory display requirements, checked by tools/build-kernel after packaging.
|
||||
# The monitor's companion SPI bridge must not be probed before the daemon claims it.
|
||||
# CONFIG_SPI_CH341 is not set
|
||||
CONFIG_USB=y
|
||||
CONFIG_USB_SUPPORT=y
|
||||
CONFIG_USB_XHCI_HCD=y
|
||||
@@ -0,0 +1 @@
|
||||
console=tty1 console=ttyAMA10,115200 rdinit=/init ro loglevel=7 drm.edid_firmware=edid/dasung-paperlike13k-37hz.bin fds.boot=normal fds.debug=1
|
||||
@@ -0,0 +1 @@
|
||||
console=tty1 rdinit=/init ro quiet loglevel=3 vt.global_cursor_default=0 drm.edid_firmware=edid/dasung-paperlike13k-37hz.bin fds.boot=normal
|
||||
@@ -0,0 +1,12 @@
|
||||
# FDS/OS Raspberry Pi 5: internal NVMe supplies boot files.
|
||||
[pi5]
|
||||
arm_64bit=1
|
||||
kernel=kernel_2712.img
|
||||
initramfs fds-initramfs.img followkernel
|
||||
dtparam=pciex1
|
||||
dtoverlay=vc4-kms-v3d,noaudio
|
||||
camera_auto_detect=0
|
||||
display_auto_detect=0
|
||||
disable_splash=1
|
||||
enable_uart=1
|
||||
[all]
|
||||
Reference in New Issue
Block a user