[perf] avoid redundant iOS native archive rebuilds

This commit is contained in:
dijunkun
2026-09-13 06:36:05 +08:00
parent ba2afc982b
commit fc921c9ca1
2 changed files with 183 additions and 9 deletions
+137
View File
@@ -0,0 +1,137 @@
"""Exercise the native build phase with fake Xmake and real Apple archives."""
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
@unittest.skipUnless(sys.platform == "darwin", "requires Apple libtool and zsh")
class NativeBuildTest(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix="crossdesk-ios-build-")
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
self.ios = self.root / "apps/ios"
scripts = self.ios / "scripts"
scripts.mkdir(parents=True)
source = Path(__file__).resolve().parents[1] / "scripts/build_minirtc_ios.sh"
self.script = scripts / source.name
shutil.copy2(source, self.script)
(self.root / "xmake.lua").write_text('-- fixture\n')
(self.root / "deps/submodules/minirtc").mkdir(parents=True)
self.bin = self.root / "bin"
self.bin.mkdir()
self.library_dir = self.root / "packages/lib"
self.library_dir.mkdir(parents=True)
obj = self.root / "fixture.o"
subprocess.run(["xcrun", "clang", "-x", "c", "-c", "-", "-o", str(obj)],
input="int fixture(void) { return 1; }", text=True, check=True)
self.archive = self.root / "fixture.a"
subprocess.run(["/usr/bin/libtool", "-static", "-o", str(self.archive), str(obj)],
check=True, capture_output=True)
links = re.search(r"REQUIRED_LINKS=\((.*?)\)", source.read_text(), re.S)[1].split()
for name in links:
shutil.copy2(self.archive, self.library_dir / f"lib{name}.a")
self.write_tool("xcodebuild", '#!/bin/sh\ncat "$(dirname "$0")/../xcode-version"\n')
self.write_tool("xcrun", '#!/bin/sh\necho fixture-iphoneos-sdk\n')
(self.root / "xcode-version").write_text("Xcode fixture-1\n")
self.write_tool("xmake", f"#!{sys.executable}\n" + r'''
import json, pathlib, shutil, sys
root = pathlib.Path(__file__).resolve().parents[1]
args = sys.argv[1:]
with (root / "calls.jsonl").open("a") as log:
log.write(json.dumps(args) + "\n")
if args[0] == "--version":
print("xmake fixture")
elif args[0] == "f":
if (root / "fail-config").exists():
sys.exit(1)
project = pathlib.Path(args[args.index("-P") + 1])
(project / "config.json").write_text(json.dumps(args))
elif args[0] == "b":
project = pathlib.Path(args[args.index("-P") + 1])
config = json.loads((project / "config.json").read_text())
output = pathlib.Path(config[config.index("-o") + 1])
output /= "iphoneos/arm64/" + config[config.index("-m") + 1]
output.mkdir(parents=True, exist_ok=True)
library = output / ("lib" + args[-1] + ".a")
if not library.exists():
shutil.copy2(root / "fixture.a", library)
elif args[0] == "show":
print(" -> " + str(root / "packages/lib") + " -> package fixture")
''')
self.env = dict(os.environ, PATH=f"{self.bin}:{os.environ['PATH']}",
XMAKE_BIN=str(self.bin / "xmake"), CONFIGURATION="Release",
CURRENT_ARCH="arm64", DEVELOPER_DIR=subprocess.check_output(
["xcode-select", "-p"], text=True).strip())
self.output = self.ios / "Vendor/iphoneos/Release/libCrossDeskMiniRTC.a"
def write_tool(self, name, contents):
path = self.bin / name
path.write_text(contents)
path.chmod(0o755)
def run_build(self, success=True):
result = subprocess.run(["/bin/zsh", str(self.script)], env=self.env,
text=True, capture_output=True)
if success:
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
else:
self.assertNotEqual(result.returncode, 0)
return result
def config_calls(self):
import json
return [args for line in (self.root / "calls.jsonl").read_text().splitlines()
if (args := json.loads(line))[0] == "f"]
def test_noop_preserves_archives_and_manifest(self):
self.run_build()
manifest = self.ios / ".xmake/wire-work/xmake.lua"
old_times = (self.output.stat().st_mtime_ns, manifest.stat().st_mtime_ns)
self.run_build()
self.assertEqual(old_times, (self.output.stat().st_mtime_ns, manifest.stat().st_mtime_ns))
self.assertEqual(["-c" in args for args in self.config_calls()], [True, True, False, False])
def test_archive_content_change_with_same_size_and_mtime_is_merged(self):
self.run_build()
original = self.output.read_bytes()
library = self.library_dir / "libopus.a"
stat = library.stat()
data = library.read_bytes()
# Rename an archive member without changing the archive layout or size.
self.assertIn(b"fixture.o", data)
library.write_bytes(data.replace(b"fixture.o", b"changed.o"))
os.utime(library, ns=(stat.st_atime_ns, stat.st_mtime_ns))
self.run_build()
self.assertNotEqual(original, self.output.read_bytes())
def test_toolchain_and_mode_changes_clear_probes(self):
self.run_build()
(self.root / "xcode-version").write_text("Xcode fixture-2\n")
self.run_build()
self.env["CONFIGURATION"] = "Debug"
self.run_build()
self.assertEqual(["-c" in args for args in self.config_calls()], [True] * 6)
self.assertTrue((self.ios / "Vendor/iphoneos/Debug/libCrossDeskMiniRTC.a").exists())
def test_failed_configuration_does_not_commit_signature(self):
self.run_build()
stamp = self.ios / ".xmake/minirtc-build/ios-config-signature"
original = stamp.read_bytes()
(self.root / "xcode-version").write_text("Xcode fixture-2\n")
(self.root / "fail-config").touch()
self.run_build(success=False)
self.assertEqual(stamp.read_bytes(), original)
(self.root / "fail-config").unlink()
self.run_build()
self.assertTrue(all("-c" in args for args in self.config_calls()))
if __name__ == "__main__":
unittest.main()