88 lines
4.2 KiB
Python
88 lines
4.2 KiB
Python
#!/usr/bin/env python3
|
||
# SPDX-License-Identifier: MPL-2.0
|
||
"""Create a new private data image; never formats a phone partition."""
|
||
import argparse
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import shlex
|
||
import shutil
|
||
import subprocess
|
||
import uuid
|
||
from persistent_state import FEATURES, digest
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument('--output', type=Path, required=True)
|
||
parser.add_argument('--system-image', type=Path, required=True)
|
||
parser.add_argument('--size-mib', type=int, default=2048)
|
||
parser.add_argument('--profile-dir', type=Path,
|
||
help='Copy an existing droidian-trial browser profile into a fresh P23 state')
|
||
args = parser.parse_args()
|
||
if args.size_mib < 512 or args.size_mib > 8192:
|
||
parser.error('Choose 512–8192 MiB')
|
||
system = json.loads(args.system_image.read_text())
|
||
base = args.system_image.parent/system['image']
|
||
if system.get('device') != 'sargo' or not system.get('filesystemCheckPassed') or digest(base) != system['imageSha256']:
|
||
raise ValueError('System image provenance mismatch')
|
||
os.umask(0o077)
|
||
out = args.output.resolve()
|
||
out.mkdir(parents=True, exist_ok=False)
|
||
seed = out/'seed'
|
||
seed.mkdir()
|
||
(seed/'system.sha256').write_text(system['imageSha256']+'\n')
|
||
for directory in ('upper','work'):
|
||
(seed/directory).mkdir(mode=0o755 if directory == 'upper' else 0o700)
|
||
(seed/'upper').chmod(0o755)
|
||
migrated = []
|
||
opt_layer = None
|
||
if args.profile_dir:
|
||
profile = args.profile_dir.resolve(strict=True)
|
||
if not profile.is_dir() or profile.is_symlink():
|
||
raise ValueError('Profile source must be a real directory')
|
||
opt_layer = seed/'upper/opt'
|
||
vulpes_layer = opt_layer/'vulpes'
|
||
profiles_layer = vulpes_layer/'profiles'
|
||
profiles_layer.mkdir(parents=True)
|
||
opt_layer.chmod(0o755)
|
||
vulpes_layer.chmod(0o755)
|
||
profiles_layer.chmod(0o755)
|
||
target = profiles_layer/'droidian-trial'
|
||
target.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copytree(profile, target, symlinks=True)
|
||
migrated = [vulpes_layer, profiles_layer, target, *target.rglob('*')]
|
||
identifier = str(uuid.uuid4())
|
||
image = out/(identifier+'.ext4')
|
||
with image.open('xb') as stream:
|
||
stream.truncate(args.size_mib*1024**2)
|
||
subprocess.run(['mke2fs','-q','-t','ext4','-F','-b','4096','-I','256','-O',FEATURES,
|
||
'-U',identifier,'-L','TUNDRA_DATA','-d',str(seed),str(image)],check=True)
|
||
# Root owns the overlay metadata, regardless of the build account's UID.
|
||
for name in ('/system.sha256','/upper','/work'):
|
||
for field in ('uid','gid'):
|
||
subprocess.run(['debugfs','-w','-R',f'set_inode_field {name} {field} 0',str(image)],
|
||
check=True,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||
if migrated:
|
||
# Gaia runs as the droidian account (UID/GID 32011) on Sargo.
|
||
commands = out/'profile-ownership.debugfs'
|
||
commands.write_text(''.join(
|
||
f'set_inode_field {shlex.quote("/upper/" + str(path.relative_to(seed/"upper")))} {field} 32011\n'
|
||
for path in migrated for field in ('uid','gid')) +
|
||
'set_inode_field /upper/opt uid 0\nset_inode_field /upper/opt gid 0\n')
|
||
subprocess.run(['debugfs','-w','-f',str(commands),str(image)],check=True,
|
||
stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|
||
commands.unlink()
|
||
check = subprocess.run(['e2fsck','-f','-n',str(image)],text=True,capture_output=True)
|
||
(out/'filesystem-check.txt').write_text(check.stdout+check.stderr)
|
||
if check.returncode:raise RuntimeError('Data image failed filesystem verification')
|
||
manifest = {'schema':1,'device':'sargo','uuid':identifier,'image':image.name,
|
||
'imageSha256':digest(image),'bytes':image.stat().st_size,
|
||
'systemImageSha256':system['imageSha256'],'filesystemCheckPassed':True,
|
||
'migratedProfile':bool(args.profile_dir),
|
||
'scope':'Private persistent overlay; containing filesystem retained; no partition formatting'}
|
||
(out/'state.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||
print(out/'state.json')
|
||
|
||
|
||
if __name__ == '__main__':main()
|