The checks guarded a local development disk floor and do not belong in the shared scripts. The scratch image capacity check in prepare-air-nir-diagnostic.py is kept because it validates a mounted image, not the host disk.
70 lines
3.4 KiB
Python
70 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Create a small mutable offline APFS Data seed from the supplied OS's initial var tree."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import plistlib
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--output', type=Path, default=ROOT / 'build/owner-data-fixture')
|
|
args = parser.parse_args()
|
|
fixture = args.output.resolve()
|
|
image = fixture / 'owner-data-32.cdr'
|
|
if image.exists():
|
|
raise SystemExit('Refuse to overwrite an existing Data seed')
|
|
source = ROOT / '.cache/firmware/target-volumes/os-mount'
|
|
if hashlib.sha256((source / 'sbin/launchd').read_bytes()).hexdigest() != '8cec9a3c44a443b08e977ccc5d577850316ad428c575dc01f09b018dbd5b6a25':
|
|
raise SystemExit('Require the unchanged supplied original OS mount')
|
|
fixture.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(['hdiutil', 'create', '-size', '32m', '-layout', 'NONE', '-fs', 'APFS',
|
|
'-volname', 'OwnerData', '-type', 'UDTO', str(image)], check=True)
|
|
target = fixture / 'mount'
|
|
target.mkdir(exist_ok=True)
|
|
attached = plistlib.loads(subprocess.check_output(['hdiutil', 'attach', '-nobrowse', '-owners', 'off',
|
|
'-mountpoint', str(target), '-plist', str(image)]))
|
|
device = attached['system-entities'][0]['dev-entry']
|
|
volume_uuid = None
|
|
count = total = 0
|
|
source = source / 'private/var'
|
|
try:
|
|
volumes = [e for e in attached['system-entities'] if e.get('mount-point') == str(target)]
|
|
if len(volumes) != 1:
|
|
raise SystemExit('Require exactly one newly formatted scratch volume')
|
|
info = plistlib.loads(subprocess.check_output(['diskutil', 'info', '-plist', volumes[0]['dev-entry']]))
|
|
volume_uuid = info['VolumeUUID']
|
|
for current, dirs, files in os.walk(source, followlinks=False):
|
|
dirs[:] = [name for name in dirs if name != 'staged_system_apps']
|
|
dest = target / Path(current).relative_to(source)
|
|
dest.mkdir(exist_ok=True)
|
|
os.chmod(dest, stat.S_IMODE(Path(current).stat().st_mode))
|
|
for name in list(dirs) + files:
|
|
src, dst = Path(current) / name, dest / name
|
|
if src.is_symlink():
|
|
os.symlink(os.readlink(src), dst)
|
|
if name in dirs:
|
|
dirs.remove(name)
|
|
elif src.is_file():
|
|
shutil.copy2(src, dst)
|
|
count += 1
|
|
total += src.stat().st_size
|
|
for name in ('keybags', 'mobile/tmp', 'tmp', 'run'):
|
|
(target / name).mkdir(parents=True, exist_ok=True)
|
|
os.chmod(target / 'tmp', 0o1777)
|
|
finally:
|
|
subprocess.run(['hdiutil', 'detach', device], check=True)
|
|
data = image.read_bytes()
|
|
if len(data) != 32 * 1024**2 or data[32:36] != b'NXSB':
|
|
raise SystemExit('Host formatting did not produce the requested raw APFS container')
|
|
report = dict(volume_uuid=volume_uuid, bytes=len(data), sha256=hashlib.sha256(data).hexdigest(), source_regular_files=count,
|
|
source_regular_bytes=total, guest_mutable=True, initial_seed_signature_verified=False,
|
|
seed='Original private/var excluding staged_system_apps; symlinks preserved; runtime directories added',
|
|
owner_keybag_created=False, os_inputs_modified=False, production_storage_qualified=False)
|
|
(fixture / 'seed.json').write_text(json.dumps(report, indent=2) + '\n')
|
|
print(json.dumps(report, indent=2))
|