mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-05 12:47:43 +00:00
154 lines
8.1 KiB
Python
154 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise the shared packaging shell with real PE imports, without executing PE code."""
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import zipfile
|
|
|
|
ROOT = Path(__file__).resolve().parent
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--cc', required=True, help='cached MinGW C compiler')
|
|
parser.add_argument('--baseline-flake', type=Path)
|
|
parser.add_argument('--output', type=Path, required=True)
|
|
args = parser.parse_args()
|
|
args.output.mkdir(parents=True, exist_ok=True)
|
|
deadline = time.monotonic() + 120
|
|
commands = []
|
|
|
|
def run(argv, *, cwd=None, env=None):
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
raise RuntimeError('Packaging check reached its 120-second deadline')
|
|
result = subprocess.run(argv, cwd=cwd, env=env, capture_output=True,
|
|
text=True, timeout=min(remaining, 30))
|
|
commands.append({'argv': list(map(str, argv)), 'returncode': result.returncode,
|
|
'stdout': result.stdout, 'stderr': result.stderr})
|
|
(args.output / 'commands.json').write_text(json.dumps(commands, indent=2) + '\n')
|
|
if result.returncode:
|
|
raise RuntimeError(f'Command failed ({result.returncode}): {argv[0]}: {result.stderr}')
|
|
return result.stdout
|
|
|
|
with tempfile.TemporaryDirectory(prefix='windows-packaging-') as temporary:
|
|
fixture = Path(temporary)
|
|
tools = fixture / 'tools'
|
|
(tools / 'bin').mkdir(parents=True)
|
|
for name in ['strings', 'zip']:
|
|
target = shutil.which(name)
|
|
if not target:
|
|
raise RuntimeError(f'Missing cached host tool: {name}')
|
|
(tools / 'bin' / name).symlink_to(target)
|
|
tree = tools / 'bin' / 'tree'
|
|
tree.write_text('#!/bin/sh\nexit 0\n')
|
|
tree.chmod(0o755)
|
|
providers = {}
|
|
for name in ['openssl', 'zlib', 'libffi', 'gmp', 'gcc', 'mcfgthreads']:
|
|
providers[name] = str(fixture / name)
|
|
(fixture / name).mkdir()
|
|
# Integer-only C fixtures establish a direct and a transitive DLL import.
|
|
# They implement no cryptography and are never executed.
|
|
sources = {
|
|
'leaf.c': 'int fixture_leaf(void) { return 17; }\n',
|
|
'first.c': 'extern int fixture_leaf(void);\nint fixture_first(void) { return fixture_leaf(); }\n',
|
|
'root.c': 'extern int fixture_first(void);\nint fixture_root(void) { return fixture_first(); }\n',
|
|
'main.c': 'extern int fixture_first(void);\nint fixture_entry(void) { return fixture_first(); }\n',
|
|
}
|
|
for name, contents in sources.items():
|
|
(fixture / name).write_text(contents)
|
|
flags = ['-std=c11', '-Werror=vla', '-pedantic', '-Wall', '-Wextra', '-nostdlib']
|
|
leaf = fixture / 'zlib' / 'fixture_leaf.dll'
|
|
first = fixture / 'openssl' / 'fixture_first.dll'
|
|
run([args.cc, *flags, '-shared', 'leaf.c', '-Wl,--entry,0',
|
|
'-Wl,--out-implib,leaf.a', '-o', str(leaf)], cwd=fixture)
|
|
run([args.cc, *flags, '-shared', 'first.c', 'leaf.a', '-Wl,--entry,0',
|
|
'-Wl,--out-implib,first.a', '-o', str(first)], cwd=fixture)
|
|
run([args.cc, *flags, '-shared', 'root.c', 'first.a', '-Wl,--entry,0',
|
|
'-Wl,--out-implib,libsimplex.dll.a', '-o', 'libsimplex.dll'], cwd=fixture)
|
|
run([args.cc, *flags, 'main.c', 'first.a', '-Wl,--entry,fixture_entry',
|
|
'-o', 'simplex-chat.exe'], cwd=fixture)
|
|
# A duplicated read-only provider must keep the first collected bytes.
|
|
duplicate = fixture / 'gcc' / first.name
|
|
shutil.copyfile(first, duplicate)
|
|
duplicate.chmod(0o444)
|
|
windows = {name: providers[name] for name in ['openssl', 'zlib', 'libffi', 'gmp']}
|
|
windows.update({'stdenv': {'cc': {'cc': providers['gcc']}},
|
|
'windows': {'mcfgthreads': providers['mcfgthreads']}})
|
|
data = {'pkgs': {'binutils': str(tools), 'tree': str(tools), 'zip': str(tools)},
|
|
'windowsPkgs': windows}
|
|
data_path = fixture / 'data.json'
|
|
data_path.write_text(json.dumps(data))
|
|
baseline = args.baseline_flake.read_text() if args.baseline_flake else None
|
|
flake = (ROOT.parent.parent / 'flake.nix').read_text()
|
|
calls = re.findall(r'(?:installPhase = old\.installPhase \+ |postInstall = )import \./scripts/nix/package-windows\.nix (\{.*?\n\s*\});', flake, re.S)
|
|
assert len(calls) == 2, 'Expected exactly the two Windows packaging callers'
|
|
results = []
|
|
for kind in ['cli', 'library']:
|
|
entry = 'simplex-chat.exe' if kind == 'cli' else 'libsimplex.dll'
|
|
expected = {entry, 'fixture_first.dll', 'fixture_leaf.dll'}
|
|
if kind == 'library':
|
|
expected.add('libsimplex.dll.a')
|
|
expression = fixture / 'render.nix'
|
|
arguments = calls[0 if kind == 'cli' else 1]
|
|
expression.write_text(f'''let
|
|
data = builtins.fromJSON (builtins.readFile {json.dumps(str(data_path))});
|
|
pkgs = data.pkgs // {{
|
|
lib.getBin = value: value;
|
|
lib.escapeShellArg = builtins.toJSON;
|
|
pkgsCross.mingwW64 = data.windowsPkgs // {{ hostPlatform.system = "fixture-windows"; }};
|
|
}};
|
|
in import {ROOT / 'package-windows.nix'} {arguments}
|
|
''')
|
|
script = run(['nix', 'eval', '--offline', '--raw', '--file', str(expression)])
|
|
variants = [('fixed', script)]
|
|
if baseline:
|
|
if kind == 'cli':
|
|
match = re.search(r'installPhase = old\.installPhase \+ \'\'(.*?)\n\s*\'\';', baseline, re.S)
|
|
else:
|
|
part = baseline[baseline.index('kind = "windows";'):]
|
|
match = re.search(r'postInstall = \'\'(.*?)\n\s*\'\';', part, re.S)
|
|
if not match:
|
|
raise RuntimeError('Cannot find exact baseline Windows packaging phase')
|
|
body = match.group(1)
|
|
body = re.sub(r'\$\{pkgs\.tree\}', str(tools), body)
|
|
body = re.sub(r'\$\{pkgs\.zip\}', str(tools), body)
|
|
body = re.sub(r'\$\{pkgs\.binutils\}', str(tools), body)
|
|
names = {'openssl': 'openssl', 'libffi': 'libffi', 'gmp': 'gmp',
|
|
'stdenv.cc.cc': 'gcc', 'windows.mcfgthreads': 'mcfgthreads'}
|
|
for nix_name, name in names.items():
|
|
body = body.replace('${pkgs.lib.getBin pkgs.pkgsCross.mingwW64.' + nix_name + '}', providers[name])
|
|
body = body.replace('${pkgs.pkgsCross.mingwW64.hostPlatform.system}', 'fixture-windows')
|
|
assert '${' not in body, 'Unresolved baseline Nix interpolation'
|
|
variants.append(('original', body))
|
|
for variant, shell in variants:
|
|
out = fixture / (kind + '-' + variant)
|
|
(out / 'bin').mkdir(parents=True)
|
|
shutil.copyfile(fixture / 'simplex-chat.exe', out / 'bin' / 'simplex-chat.exe')
|
|
env = dict(os.environ, out=str(out))
|
|
run(['/bin/bash', '-e', '-c', shell], cwd=fixture, env=env)
|
|
archive_path = next(out.glob('*.zip'))
|
|
with zipfile.ZipFile(archive_path) as archive:
|
|
assert archive.testzip() is None
|
|
actual = set(archive.namelist())
|
|
if variant == 'fixed':
|
|
assert actual == expected, (kind, actual, expected)
|
|
else:
|
|
assert expected - actual, 'Original unexpectedly includes the DLL closure'
|
|
results.append({'kind': kind, 'variant': variant, 'members': sorted(actual),
|
|
'missingExpectedDLLs': sorted(expected - actual)})
|
|
(args.output / 'result.json').write_text(json.dumps({'checks': results,
|
|
'realPEInputs': True, 'targetExecuted': False}, indent=2) + '\n')
|
|
print(json.dumps(results, indent=2))
|
|
(args.output / 'commands.json').write_text(json.dumps(commands, indent=2) + '\n')
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|