mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-11-08 23:07:25 +00:00
214 lines
9.2 KiB
Python
214 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Check mobile linker argv and, optionally, MinGW archive extraction.
|
|
|
|
The optional PE link control uses the pinned GHC initializer body with a tiny
|
|
fixture-only HsIface layout. It checks BFD archive extraction, not GHC ABI or
|
|
Windows execution.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
|
|
|
|
SCRIPTS = Path(__file__).resolve().parent
|
|
ROOT_OPTION = "-optl-Wl,-u,init_ghc_hs_iface"
|
|
|
|
|
|
class MobileLinkArguments(unittest.TestCase):
|
|
def invoke(self, kind, status=0):
|
|
with tempfile.TemporaryDirectory(prefix="mobile-link-argv-") as directory:
|
|
root = Path(directory)
|
|
library = root / "lib"
|
|
library.mkdir()
|
|
(library / "libHSfixture.a").touch()
|
|
compiler = root / "ghc"
|
|
compiler.write_text(
|
|
"#!/usr/bin/env python3\nimport json,os,sys\n"
|
|
"if sys.argv[1:]==['--numeric-version']:\n"
|
|
" print('9.14.1');sys.exit(0)\n"
|
|
"with open(os.environ['ARGV_LOG'],'a') as f:\n"
|
|
" f.write(json.dumps(sys.argv[1:])+'\\n')\n"
|
|
"sys.exit(int(os.environ['COMPILER_STATUS']))\n"
|
|
)
|
|
compiler.chmod(0o755)
|
|
package_tool = root / "ghc-pkg"
|
|
package_tool.write_text(
|
|
"#!/usr/bin/env python3\nimport os,sys\n"
|
|
"field=sys.argv[-2]\n"
|
|
"print({'id':'fixture-unit','hs-libraries':'HSfixture',"
|
|
"'library-dirs':os.environ['FIXTURE_LIBRARY']}[field])\n"
|
|
)
|
|
package_tool.chmod(0o755)
|
|
log = root / "argv.jsonl"
|
|
database = root / "package.conf.d"
|
|
database.mkdir()
|
|
output = root / "bundle"
|
|
environment = os.environ.copy()
|
|
environment.pop("MOBILE_LIBRARY_PACKAGE_DBS", None)
|
|
environment.update(ARGV_LOG=str(log), FIXTURE_LIBRARY=str(library),
|
|
COMPILER_STATUS=str(status))
|
|
result = subprocess.run(
|
|
["bash", str(SCRIPTS / "link-mobile-library.sh"),
|
|
str(compiler), str(package_tool), str(database), "fixture",
|
|
kind, str(output), "-optl-example with spaces"],
|
|
env=environment, capture_output=True, text=True, timeout=15,
|
|
)
|
|
argv = json.loads(log.read_text()) if log.exists() else None
|
|
common = ["-v0", "-no-hs-main", "-hide-all-packages",
|
|
"-package-db", str(database)]
|
|
return result, argv, common, str(library / "libHSfixture.a")
|
|
|
|
def test_windows_roots_only_the_real_initializer(self):
|
|
result, argv, common, _ = self.invoke("windows")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(argv, common + [
|
|
"-shared", "-threaded", "-flink-rts", "-package-id", "fixture-unit",
|
|
ROOT_OPTION, "-optl-example with spaces", "MobileBundle.hs",
|
|
"-o", "libsimplex.dll",
|
|
])
|
|
|
|
def test_android_argv_is_unchanged(self):
|
|
result, argv, common, _ = self.invoke("android")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(argv, common + [
|
|
"-shared", "-threaded", "-flink-rts", "-package-id", "fixture-unit",
|
|
"-optl-example with spaces", "MobileBundle.hs", "-o", "libsimplex.so",
|
|
])
|
|
|
|
def test_support_does_not_link_a_second_rts(self):
|
|
result, argv, common, archive = self.invoke("android-support")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(argv, common + [
|
|
"-shared", "-fno-link-rts", "-no-auto-link-packages",
|
|
"-optl-example with spaces", archive, "MobileBundle.hs",
|
|
"-o", "libsupport.so",
|
|
])
|
|
|
|
def test_ios_argv_is_unchanged(self):
|
|
result, argv, common, _ = self.invoke("ios")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(argv, common + [
|
|
"-staticlib", "-threaded", "-package-id", "fixture-unit",
|
|
"-optl-example with spaces", "MobileBundle.hs",
|
|
"-o", "dist/libHSfixture-ghc9.14.1.a",
|
|
])
|
|
|
|
def test_compiler_failure_is_preserved(self):
|
|
result, _, _, _ = self.invoke("windows", status=17)
|
|
self.assertEqual(result.returncode, 17)
|
|
|
|
def test_unknown_kind_is_rejected(self):
|
|
result, argv, _, _ = self.invoke("unknown")
|
|
self.assertEqual(result.returncode, 2)
|
|
self.assertIsNone(argv)
|
|
|
|
|
|
def archive_control(args):
|
|
root = args.link_output.resolve()
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
source = args.ghc_source / "libraries/ghc-internal/cbits/RtsIface.c"
|
|
contents = source.read_text()
|
|
signature = "void init_ghc_hs_iface(void)\n{"
|
|
if contents.count(signature) != 1:
|
|
raise RuntimeError("expected the one pinned GHC initializer definition")
|
|
function = contents[contents.index(signature):]
|
|
# Preserve the upstream copyright and exact initializer body. Only the
|
|
# interface layout and RTS caller are miniature archive-order fixtures.
|
|
copyright_header = contents[:contents.index('#include "Rts.h"')]
|
|
(root / "Iface.h").write_text(
|
|
"typedef struct { int marker; } HsIface;\n"
|
|
"extern HsIface *ghc_hs_iface;\n"
|
|
"void init_ghc_hs_iface(void);\nvoid hs_init_fixture(void);\n"
|
|
"void mobile_probe(void);\n"
|
|
)
|
|
(root / "RtsIface.c").write_text(
|
|
copyright_header + '#include <stddef.h>\n#include "Iface.h"\n'
|
|
"static HsIface the_ghc_hs_iface = {1};\n" + function
|
|
)
|
|
(root / "RtsStartup.c").write_text(
|
|
'#include <stddef.h>\n#include "Iface.h"\n'
|
|
"HsIface *ghc_hs_iface = NULL;\n"
|
|
"void hs_init_fixture(void) { init_ghc_hs_iface(); }\n"
|
|
)
|
|
(root / "Bundle.c").write_text(
|
|
'#include "Iface.h"\n'
|
|
"void mobile_probe(void) { hs_init_fixture(); }\n"
|
|
)
|
|
(root / "Bundle.def").write_text("EXPORTS\n mobile_probe\n")
|
|
commands = []
|
|
deadline = time.monotonic() + 120
|
|
|
|
def run(argv):
|
|
remaining = deadline - time.monotonic()
|
|
if remaining <= 0:
|
|
raise TimeoutError("archive control reached its wall-clock limit")
|
|
result = subprocess.run(argv, cwd=root, capture_output=True, text=True,
|
|
timeout=min(30, remaining))
|
|
commands.append({"argv": argv, "returncode": result.returncode,
|
|
"output": result.stdout + result.stderr})
|
|
(root / "commands.json").write_text(json.dumps(commands, indent=2) + "\n")
|
|
return result
|
|
|
|
flags = ["-std=c11", "-Werror=vla", "-pedantic", "-Wall", "-Wextra", "-O0"]
|
|
for name in ["RtsIface", "RtsStartup", "Bundle"]:
|
|
run([args.cc, *flags, "-c", name + ".c", "-o", name + ".o"]).check_returncode()
|
|
for name, member in [("ghc-internal", "RtsIface"), ("rts", "RtsStartup")]:
|
|
run([args.ar, "rcs", "lib" + name + ".a", member + ".o"]).check_returncode()
|
|
results = {}
|
|
for variant in ["original", "prefix", "suffix", "missing-provider"]:
|
|
root_flag = ["-Wl,-u,init_ghc_hs_iface"]
|
|
argv = [args.cc, "-shared", "Bundle.o", "Bundle.def", *args.link_option]
|
|
if variant == "prefix":
|
|
argv += root_flag
|
|
if variant != "missing-provider":
|
|
argv += ["libghc-internal.a"]
|
|
argv += ["librts.a"]
|
|
if variant in ["suffix", "missing-provider"]:
|
|
argv += root_flag
|
|
argv += ["-Wl,-Map," + variant + ".map", "-o", variant + ".dll"]
|
|
result = run(argv)
|
|
results[variant] = result.returncode
|
|
if variant in ["original", "missing-provider"]:
|
|
if (result.returncode != 1 or "undefined reference" not in result.stderr
|
|
or "init_ghc_hs_iface" not in result.stderr):
|
|
raise RuntimeError("expected the specific missing-initializer link failure")
|
|
else:
|
|
result.check_returncode()
|
|
if "libghc-internal.a(RtsIface.o)" not in (root / (variant + ".map")).read_text():
|
|
raise RuntimeError("link map did not extract the real initializer body")
|
|
receipt = {"results": results, "source": str(source),
|
|
"initializerSHA256": hashlib.sha256(function.encode()).hexdigest(),
|
|
"qualification": "PE archive extraction only; no GHC ABI/runtime execution"}
|
|
(root / "result.json").write_text(json.dumps(receipt, indent=2) + "\n")
|
|
print(json.dumps(receipt))
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("--cc", help="also run the real MinGW archive link control")
|
|
parser.add_argument("--ar")
|
|
parser.add_argument("--ghc-source", type=Path)
|
|
parser.add_argument("--link-output", type=Path)
|
|
parser.add_argument("--link-option", action="append", default=[])
|
|
args = parser.parse_args()
|
|
if args.cc and not all([args.ar, args.ghc_source, args.link_output]):
|
|
parser.error("--cc also requires --ar, --ghc-source and --link-output")
|
|
suite = unittest.defaultTestLoader.loadTestsFromTestCase(MobileLinkArguments)
|
|
if not unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful():
|
|
return 1
|
|
if args.cc:
|
|
archive_control(args)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|