mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-16 17:10:20 +08:00
[perf] avoid redundant iOS native archive rebuilds
This commit is contained in:
@@ -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}"
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user