initial commit
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user