Files
darling-lol/scripts/nix-state.py
T

167 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Per-prefix rootless readiness; no Darling execution or prefix writes."""
import hashlib
import json
import os
from pathlib import Path
import pwd
import sys
sys.dont_write_bytecode = True
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / 'tools'))
from runtime_paths import runtime_paths
def selected_paths():
config_path = ROOT / 'build/config.json'
config = json.loads(config_path.read_text()) if config_path.exists() else {}
guest = config.get('guest_user') or pwd.getpwuid(os.getuid()).pw_name
config.setdefault('prefix', str(Path.home() / '.darling'))
config.setdefault('guest_home', '/Users/' + guest)
configured_prefix = Path(os.environ.get('DPREFIX') or config['prefix']).resolve()
env_prefix = os.environ.get('DPREFIX')
try:
os.environ['DPREFIX'] = str(configured_prefix)
prefix, home = runtime_paths(config)
finally:
if env_prefix is None:
os.environ.pop('DPREFIX', None)
else:
os.environ['DPREFIX'] = env_prefix
return prefix.resolve(), home.resolve(), guest
def guest_user():
return selected_paths()[2]
def digest(paths):
result = hashlib.sha256()
for path in sorted(paths):
if path.is_file():
result.update(str(path.relative_to(ROOT)).encode() + b'\0' + path.read_bytes())
return result.hexdigest()
def base_identity():
runtime_tree = ROOT / 'usr-local/libexec/darling'
runtime_modes = {name: (ROOT / 'usr-local/bin' / name).stat().st_mode & 0o777
if (ROOT / 'usr-local/bin' / name).exists() else None
for name in ('darling', 'darlingserver')}
return {'schema': 2, 'package': os.environ.get('DARLING_PACKAGE_ID', ''),
'runtime_modes': runtime_modes,
'runtime_tree': inventory(runtime_tree),
'sources': digest([* (ROOT / 'patches').glob('*.patch'),
ROOT / 'upstream/manifest.json',
* (ROOT / 'scripts').glob('*.py'),
* (ROOT / 'scripts').glob('*.sh'),
* (ROOT / 'tools').glob('*')]),
'runtime': digest([ROOT / 'usr-local/bin/darling',
ROOT / 'usr-local/bin/darlingserver'])}
def inventory(root):
"""Include all required trees and modes without hashing large installed binaries."""
if not root.is_dir():
return None
result = hashlib.sha256()
def enumeration_error(error):
raise error
for directory, dirs, files in os.walk(root, followlinks=False,
onerror=enumeration_error):
excluded = ('.before-', '.rt-', '.llvm')
dirs[:] = sorted(name for name in dirs if not any(token in name for token in excluded))
for name in sorted(dirs + files):
if any(token in name for token in excluded):
continue
path = Path(directory) / name
info = path.lstat()
target_str = ''
if path.is_symlink():
try:
target_str = f':{os.readlink(path)}'
except OSError:
pass
result.update(str(path.relative_to(root)).encode() + b'\0')
result.update(f'{info.st_mode}:{info.st_size}:{info.st_mtime_ns}{target_str}'.encode())
return result.hexdigest()
def overlay_identity(home):
result: dict[str, object] = {name: inventory(home / name) for name in
('RuntimeLibraries', 'RuntimeFrameworks',
'Library/Audio/Plug-Ins/Components/DarlingRiotAudio.component')}
helper = home / 'libDarlingXShm.dylib'
result['xshm'] = ([helper.stat().st_mode, helper.stat().st_size,
helper.stat().st_mtime_ns] if helper.is_file() else None)
return result
def identity():
prefix, home, guest = selected_paths()
return dict(base_identity(), prefix=str(prefix), guest=guest, guest_home=str(home),
overlays=overlay_identity(home), uid=os.getuid(), gid=os.getgid())
def read(path):
try:
return json.loads(path.read_text())
except (OSError, ValueError):
return None
def atomic(path, value):
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_suffix('.tmp')
temporary.write_text(json.dumps(value, sort_keys=True) + '\n')
temporary.replace(path)
def main():
action = sys.argv[1]
state = identity()
key = hashlib.sha256(state['prefix'].encode()).hexdigest()
marker = ROOT / 'build/nix-ready' / (key + '.json')
base_marker = ROOT / 'build/nix-base.json'
if action == 'guest':
print(state['guest'])
elif action == 'check-base':
return 0 if read(base_marker) == base_identity() else 1
elif action == 'check':
return 0 if read(marker) == state and read(base_marker) == base_identity() else 1
elif action == 'stamp-base':
if (not all((ROOT / 'usr-local/bin' / name).is_file() and
os.access(ROOT / 'usr-local/bin' / name, os.X_OK)
for name in ('darling', 'darlingserver')) or
not (ROOT / 'usr-local/libexec/darling').is_dir() or not state['package']):
raise SystemExit('Cannot stamp readiness without executable base runtime, SDK and package identity')
atomic(base_marker, base_identity())
elif action == 'stamp':
if read(base_marker) != base_identity():
raise SystemExit('Base runtime readiness is missing; run darling-lol --update in a terminal.')
home = Path(str(state['guest_home']))
if not home.is_dir() or not any((home / 'RuntimeLibraries').glob('*')) or not any((home / 'RuntimeFrameworks').glob('*')):
raise SystemExit('Cannot stamp an empty or missing prefix runtime')
# Reuse the complete installation gate; readiness must never mint an
# apparently valid manifest for partial overlays, even when called directly.
import importlib.util
spec = importlib.util.spec_from_file_location('runtime_validation', ROOT / 'tools/validate-riot-runtime.py')
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.inputs(Path(str(state['prefix'])), home)
atomic(marker, state)
elif action in ('invalidate', 'invalidate-base'):
marker.unlink(missing_ok=True)
if action == 'invalidate-base':
base_marker.unlink(missing_ok=True)
else:
raise SystemExit(f'Unknown state action: {action}')
return 0
if __name__ == '__main__':
raise SystemExit(main())