Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/app-host-test-rerun.yml
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,9 @@ jobs:
--package-root "$PWD/Packages" \
--package-root "$PWD/vendor" \
--package-root "$PWD/Examples" \
--package-root "$PWD/.ci-source-packages/checkouts"
--package-root "$PWD/.ci-source-packages/checkouts" \
--xcframework-root "$PWD/.ci-source-packages/artifacts" \
--arch "$PRODUCT_ARCH"
rm -f "$bundle/Contents/MacOS/cmuxTests"
compile() {
xcodebuild -project cmux.xcodeproj -target cmuxTests -configuration Debug \
Expand Down
36 changes: 35 additions & 1 deletion scripts/ci/app_host_test_rerun.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import argparse
import json
import plistlib
import os
import re
import subprocess
Expand Down Expand Up @@ -442,6 +443,32 @@ def locate_c_targets(names: list[str], roots: list[Path], dump: Callable[[Path],
return found


def binary_framework_search_paths(roots: list[Path], arch: str | None) -> list[str]:
"""Use resolved macOS XCFramework slices when archived runtime copies lack modules."""
if roots and not arch:
raise ValueError("binary framework selection requires the product architecture")
paths = set()
for root in roots:
if not root.is_dir():
raise ValueError(f"resolved binary artifact directory is missing: {root}")
for manifest in sorted(root.glob("**/*.xcframework/Info.plist")):
with manifest.open("rb") as stream:
libraries = plistlib.load(stream).get("AvailableLibraries", [])
for library in libraries:
if (library.get("SupportedPlatform") != "macos"
or library.get("SupportedPlatformVariant")
or arch not in library.get("SupportedArchitectures", [])):
continue
relative = Path(library["LibraryIdentifier"]) / library["LibraryPath"]
if relative.suffix != ".framework":
continue
framework = (manifest.parent / relative).resolve()
if not framework.is_relative_to(manifest.parent.resolve()) or not framework.is_dir():
raise ValueError(f"invalid binary framework slice: {framework}")
paths.add(str(framework.parent))
return sorted(paths)


def detach(args: argparse.Namespace, dump: Callable[[Path], dict] = dump_package) -> None:
project = Path(args.project)
derived = Path(args.derived_data)
Expand All @@ -467,7 +494,12 @@ def detach(args: argparse.Namespace, dump: Callable[[Path], dict] = dump_package
project.write_text(text)
quote = lambda value: '"' + value.replace('"', '\\"') + '"' if re.search(r"\s", value) else value
settings = {
"FRAMEWORK_SEARCH_PATHS": ["$(inherited)", "$(BUILT_PRODUCTS_DIR)/PackageFrameworks"],
"FRAMEWORK_SEARCH_PATHS": [
"$(inherited)", "$(BUILT_PRODUCTS_DIR)/PackageFrameworks",
*binary_framework_search_paths(
[Path(root) for root in getattr(args, "xcframework_root", [])], getattr(args, "arch", None)
),
],
"OTHER_LDFLAGS": ["$(inherited)", *ldflags],
"OTHER_SWIFT_FLAGS": ["$(inherited)", *swift_flags],
}
Expand Down Expand Up @@ -630,6 +662,8 @@ def main(argv: list[str] | None = None) -> int:
detach_parser.add_argument("--xcconfig", required=True)
detach_parser.add_argument("--target", default=TEST_TARGET)
detach_parser.add_argument("--package-root", action="append", default=[])
detach_parser.add_argument("--xcframework-root", action="append", default=[])
detach_parser.add_argument("--arch", help="Architecture recorded by the adopted product")
prune_parser = commands.add_parser("prune")
prune_parser.add_argument("--project", required=True)
prune_parser.add_argument("--test-root", required=True)
Expand Down
50 changes: 50 additions & 0 deletions tests/test_app_host_test_rerun.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import argparse
import os
import plistlib
import shlex
import re
import subprocess
import sys
Expand Down Expand Up @@ -440,6 +442,54 @@ def test_generates_module_maps_for_package_c_targets(self) -> None:
self.assertIn("-framework Pkg_1_PackageProduct", xcconfig)
self.assertIn(f'"{host / "MacOS" / "Host App.debug.dylib"}"', xcconfig)

def test_detach_imports_only_matching_resolved_binary_framework_slices(self) -> None:
with tempfile.TemporaryDirectory(prefix="rerun products ") as directory:
root = Path(directory)
debug = root / "Build" / "Products" / "Debug"
(debug / "PackageFrameworks" / "Pkg_1_PackageProduct.framework").mkdir(parents=True)
host = debug / "Host App.app" / "Contents"
(host / "PlugIns" / "cmuxTests.xctest").mkdir(parents=True)
# Runtime copies can survive while Xcode removes importable build products.
(host / "Frameworks" / "Sparkle.framework").mkdir(parents=True)
artifacts = root / "resolved artifacts"
expected = []
excluded = []
for name in ("Sparkle", "Iroh"):
xcframework = artifacts / name / f"{name}.xcframework"
libraries = []
for identifier, platform, architectures, variant in (
("macos-universal", "macos", ["arm64", "x86_64"], None),
("macos-intel", "macos", ["x86_64"], None),
("ios-arm64", "ios", ["arm64"], None),
("ios-catalyst", "ios", ["arm64"], "maccatalyst"),
):
framework = xcframework / identifier / f"{name}.framework"
(framework / "Modules").mkdir(parents=True)
(framework / "Modules" / "module.modulemap").write_text(f"framework module {name} {{}}")
library = dict(LibraryIdentifier=identifier, LibraryPath=f"{name}.framework",
SupportedPlatform=platform, SupportedArchitectures=architectures)
if variant:
library["SupportedPlatformVariant"] = variant
libraries.append(library)
(expected if identifier == "macos-universal" else excluded).append(framework.parent)
(xcframework / "Info.plist").write_bytes(plistlib.dumps(dict(AvailableLibraries=libraries)))
project = root / "project.pbxproj"
project.write_text(PROJECT)
config = root / "detached.xcconfig"
args = argparse.Namespace(
project=str(project), derived_data=str(root), xcconfig=str(config), target="cmuxTests",
package_root=[], xcframework_root=[str(artifacts)], arch="arm64",
)
with unittest.mock.patch("sys.stdout"):
rerun.detach(args)
settings = dict(line.split(" = ", 1) for line in config.read_text().splitlines())
search_paths = shlex.split(settings["FRAMEWORK_SEARCH_PATHS"])
for directory in expected:
self.assertIn(str(directory), search_paths)
for directory in excluded:
self.assertNotIn(str(directory), search_paths)
self.assertNotIn(str(host / "Frameworks"), search_paths)

def test_umbrella_header_wins_over_directory(self) -> None:
with tempfile.TemporaryDirectory() as directory:
include = Path(directory)
Expand Down
Loading