From fc921c9ca1f127c9eb8955143229f2c8430f4376 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Sun, 13 Sep 2026 06:36:05 +0800 Subject: [PATCH] [perf] avoid redundant iOS native archive rebuilds --- apps/ios/scripts/build_minirtc_ios.sh | 55 +++++++-- apps/ios/tests/build_minirtc_ios_test.py | 137 +++++++++++++++++++++++ 2 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 apps/ios/tests/build_minirtc_ios_test.py diff --git a/apps/ios/scripts/build_minirtc_ios.sh b/apps/ios/scripts/build_minirtc_ios.sh index 742b84c9..4ec18f43 100755 --- a/apps/ios/scripts/build_minirtc_ios.sh +++ b/apps/ios/scripts/build_minirtc_ios.sh @@ -77,17 +77,42 @@ WIRE_MANIFEST_FILE="${WIRE_WORK_DIR}/xmake.lua" WIRE_LIBRARY="${WIRE_BUILD_DIR}/iphoneos/arm64/${MODE}/libcrossdesk_wire.a" mkdir -p "${OUTPUT_DIR}" "${WIRE_WORK_DIR}" -cp "${REPO_DIR}/xmake.lua" "${WIRE_MANIFEST_FILE}" +if ! cmp -s "${REPO_DIR}/xmake.lua" "${WIRE_MANIFEST_FILE}"; then + cp "${REPO_DIR}/xmake.lua" "${WIRE_MANIFEST_FILE}" +fi + +# Reuse Xmake's compiler/dependency probes unless the build environment changes. +# Still configure on every invocation: Xmake must see manifest edits and restore +# the iOS configuration if MiniRTC was built standalone for another platform. +CONFIG_SIGNATURE="$(print -r -- "${XMAKE_BIN}" "${XMAKE_DEVELOPER_DIR}" \ + "${MODE}" "${ARCH_NAME}" "16.0" "${XMAKE_PKG_INSTALLDIR}" + run_xmake --version | sed -n '1p' + DEVELOPER_DIR="${XMAKE_DEVELOPER_DIR}" xcodebuild -version + DEVELOPER_DIR="${XMAKE_DEVELOPER_DIR}" xcrun --sdk iphoneos --show-sdk-path + DEVELOPER_DIR="${XMAKE_DEVELOPER_DIR}" xcrun --sdk iphoneos --show-sdk-version)" + +configure_xmake() { + local project_dir="$1" + local build_dir="$2" + shift 2 + local stamp="${build_dir}/ios-config-signature" + local clean_flags=() + if [[ ! -f "${stamp}" || "$(<"${stamp}")" != "${CONFIG_SIGNATURE}" ]]; then + clean_flags=(-c) + fi + run_xmake f -P "${project_dir}" "${clean_flags[@]}" -o "${build_dir}" \ + -p iphoneos -a arm64 -m "${MODE}" --target_minver=16.0 -y "$@" + mkdir -p "${build_dir}" + print -r -- "${CONFIG_SIGNATURE}" > "${stamp}" +} # Xmake stores the configured build directory relative to the process working # directory. Xcode does not guarantee that directory for build phases, so keep # configuration, compilation and inspection anchored to the MiniRTC project. ( cd "${MINIRTC_DIR}" - run_xmake f -P "${MINIRTC_DIR}" -c -o "${MINIRTC_BUILD_DIR}" \ - -p iphoneos -a arm64 -m "${MODE}" \ - --as="${XMAKE_TOOLCHAIN_BIN}/clang" \ - --target_minver=16.0 --USE_CUDA=false -y + configure_xmake "${MINIRTC_DIR}" "${MINIRTC_BUILD_DIR}" \ + --as="${XMAKE_TOOLCHAIN_BIN}/clang" --USE_CUDA=false run_xmake b -P "${MINIRTC_DIR}" minirtc ) @@ -101,9 +126,7 @@ fi # no second project definition to maintain. ( cd "${WIRE_WORK_DIR}" - run_xmake f -P "${WIRE_WORK_DIR}" -c -o "${WIRE_BUILD_DIR}" \ - -p iphoneos -a arm64 -m "${MODE}" \ - --target_minver=16.0 -y + configure_xmake "${WIRE_WORK_DIR}" "${WIRE_BUILD_DIR}" run_xmake b -P "${WIRE_WORK_DIR}" crossdesk_wire ) @@ -144,10 +167,24 @@ for link_name in "${REQUIRED_LINKS[@]}"; do done TEMP_LIBRARY="${OUTPUT_LIBRARY}.tmp" -rm -f "${TEMP_LIBRARY}" +MERGE_MANIFEST="${OUTPUT_LIBRARY}.inputs.sha256" +TEMP_MANIFEST="${MERGE_MANIFEST}.tmp" +trap 'rm -f "${TEMP_LIBRARY}" "${TEMP_MANIFEST}"' EXIT + +# Hash contents as well as paths so same-size archives replaced within the same +# timestamp tick cannot leave a stale merged library. Preserve the output mtime +# on no-op builds to avoid relinking the Xcode app. +shasum -a 256 "${SCRIPT_DIR}/build_minirtc_ios.sh" "${MINIRTC_LIBRARY}" \ + "${DEPENDENCY_ARCHIVES[@]}" "${WIRE_LIBRARY}" > "${TEMP_MANIFEST}" +if [[ -f "${OUTPUT_LIBRARY}" ]] && cmp -s "${TEMP_MANIFEST}" "${MERGE_MANIFEST}"; then + print "Up to date: ${OUTPUT_LIBRARY}" + exit 0 +fi + /usr/bin/libtool -static -o "${TEMP_LIBRARY}" \ "${MINIRTC_LIBRARY}" "${DEPENDENCY_ARCHIVES[@]}" \ "${WIRE_LIBRARY}" mv -f "${TEMP_LIBRARY}" "${OUTPUT_LIBRARY}" +mv -f "${TEMP_MANIFEST}" "${MERGE_MANIFEST}" print "Created ${OUTPUT_LIBRARY}" diff --git a/apps/ios/tests/build_minirtc_ios_test.py b/apps/ios/tests/build_minirtc_ios_test.py new file mode 100644 index 00000000..f1aaf18c --- /dev/null +++ b/apps/ios/tests/build_minirtc_ios_test.py @@ -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()