#!/usr/bin/env python3 """Validate the installed local deb with original SDK/build prefixes hidden. Runs only in the dedicated Ubuntu guest. Root is used for a private mount namespace; the viewer, compositor, and document workers run as docview. """ import argparse import hashlib import json import os from pathlib import Path import re import subprocess import sys import tempfile ROOT = Path(__file__).resolve().parents[2] APP = Path('/opt/docview') HIDDEN = ['/opt/docview-qt', '/opt/docview-deps', '/home/docview/docview-install', '/home/docview/docview-build'] def sha(path): with path.open('rb') as stream: return hashlib.file_digest(stream, 'sha256').hexdigest() def user_run(output): assert os.getuid() != 0 assert not any(name in os.environ for name in ('LD_LIBRARY_PATH', 'QT_PLUGIN_PATH', 'QT_QPA_PLATFORM_PLUGIN_PATH', 'QML_IMPORT_PATH', 'QML2_IMPORT_PATH', 'QTWEBENGINEPROCESS_PATH', 'QTWEBENGINE_DISABLE_SANDBOX')) hidden = {name: not Path(name).exists() or not any(Path(name).iterdir()) for name in HIDDEN} assert all(hidden.values()) manifest = json.loads((APP / 'share/doc/docview/package-manifest.json').read_text()) installed = [] for row in manifest['files']: if row['path'].startswith('DEBIAN/'): continue path = Path('/') / row['path'] value = path.stat() assert value.st_uid == 0 and value.st_gid == 0 assert oct(value.st_mode & 0o7777) == row['mode'] assert value.st_size == row['size'] and sha(path) == row['sha256'] installed.append(row['path']) environment = {**os.environ, 'LD_LIBRARY_PATH': str(APP / 'lib') + ':' + str(APP / 'qt/lib')} dependencies = [] for path in sorted(APP.rglob('*')): if not path.is_file(): continue with path.open('rb') as stream: if stream.read(4) != b'\x7fELF': continue result = subprocess.run(['ldd', str(path)], env=environment, capture_output=True, text=True, timeout=30) assert result.returncode == 0 and 'not found' not in result.stdout, str(path) resolved = re.findall(r'(?:=>\s+)?(/[^\s]+)\s+\(', result.stdout) no_needed = False if not resolved: dynamic = subprocess.check_output(['readelf', '-d', str(path)], text=True, timeout=30) no_needed = '(NEEDED)' not in dynamic and result.stdout.strip() == 'statically linked' assert (resolved or no_needed) and all(name.startswith(('/opt/docview/', '/lib/', '/lib64/', '/usr/lib/')) for name in resolved), str(path) + ': ' + result.stdout dependencies.append({'path': str(path), 'sha256': sha(path), 'resolved': sorted(set(resolved)), 'noDynamicDependencies': no_needed}) identities = {name: sha(APP / 'bin' / name) for name in ['docview', 'docview-pdf-worker', 'docview-archive-worker']} launcher = sha(Path('/usr/bin/docview')) record = {'success': False, 'hiddenOriginalPrefixes': hidden, 'uid': os.getuid(), 'callerRuntimeVariablesAbsent': True, 'installedFilesVerified': len(installed), 'executables': identities, 'launcherSha256': launcher, 'elfDependencies': dependencies, 'runnerSha256': sha(Path(__file__)), 'smokeSourceSha256': sha(ROOT / 'tests/smoke.py'), 'waylandRunnerSha256': sha(ROOT / 'tests/run_wayland_validation.py'), 'scope': 'Dedicated existing Ubuntu VM, private mount namespace, Xvfb and headless Sway/software; not a clean OS or physical desktop'} commands = [ ('x11', ['dbus-run-session', '--', 'xvfb-run', '-a', '-s', '-screen 0 1100x760x24', sys.executable, str(ROOT / 'tests/smoke.py'), '--binary', '/usr/bin/docview', '--output', str(output / 'x11')]), ('wayland', [sys.executable, str(ROOT / 'tests/run_wayland_validation.py'), '--build-dir', '/opt/docview/bin', '--smoke-binary', '/usr/bin/docview', '--output', str(output / 'wayland'), '--mode', 'smoke'])] runs = [] try: for name, command in commands: env = dict(os.environ) if name == 'x11': env.update(QT_QPA_PLATFORM='xcb', QT_QUICK_BACKEND='software', QTWEBENGINE_CHROMIUM_FLAGS='--disable-gpu') with (output / (name + '.log')).open('w') as log: result = subprocess.run(command, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=300) runs.append({'name': name, 'exitCode': result.returncode}) assert result.returncode == 0, name + ' smoke failed' values = json.loads((output / name / ('smoke/results.json' if name == 'wayland' else 'results.json')).read_text()) assert len(values) == 6 and all(row['state'] == 'Ready' and row['binarySha256'] == launcher for row in values) assert all(sha(APP / 'bin' / name) == digest for name, digest in identities.items()) assert sha(Path('/usr/bin/docview')) == launcher record.update(success=True, executableBytesUnchanged=True, smokeConditions=12) finally: record['runs'] = runs (output / 'report.json').write_text(json.dumps(record, indent=2, sort_keys=True) + '\n') print(json.dumps({key: record[key] for key in ('success', 'installedFilesVerified', 'smokeConditions')})) def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--output', type=Path, required=True) parser.add_argument('--isolated', action='store_true') parser.add_argument('--user-run', action='store_true') parser.add_argument('--hide-prefix', action='append', default=[], type=Path, help='Additional dedicated guest build/install prefix to hide in the private namespace') args = parser.parse_args() output = args.output.resolve() assert len(args.hide_prefix) <= 8 additional = [] for prefix in args.hide_prefix: assert prefix.is_absolute() and not prefix.is_symlink() prefix = prefix.absolute() assert prefix.is_relative_to('/home/docview') and prefix != Path('/home/docview') assert '..' not in prefix.parts and not ROOT.is_relative_to(prefix) and not output.is_relative_to(prefix) additional.extend(['--hide-prefix', str(prefix)]) if str(prefix) not in HIDDEN: HIDDEN.append(str(prefix)) if args.user_run: user_run(output) elif args.isolated: assert os.getuid() == 0 with tempfile.TemporaryDirectory(prefix='docview-hidden-runtime-') as temporary: Path(temporary).chmod(0o755) for name in HIDDEN: if Path(name).is_dir(): subprocess.run(['mount', '--bind', temporary, name], check=True) subprocess.run(['runuser', '-u', 'docview', '--', 'env', '-i', 'HOME=/home/docview', 'USER=docview', 'LOGNAME=docview', 'PATH=/usr/bin:/bin', 'LANG=C.UTF-8', sys.executable, str(Path(__file__)), '--user-run', '--output', str(output), *additional], check=True) else: assert os.getuid() != 0 and Path.home() == Path('/home/docview') output.mkdir(parents=True, exist_ok=False) subprocess.run(['sudo', 'unshare', '--mount', '--propagation', 'private', sys.executable, str(Path(__file__)), '--isolated', '--output', str(output), *additional], check=True) if __name__ == '__main__': main()