[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
+46 -9
View File
@@ -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}"
+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()