Skip to content

Commit 4ea8cbd

Browse files
authored
Support binary XCFramework dependencies in SPM build system (#75)
1 parent 28ccdaf commit 4ea8cbd

4 files changed

Lines changed: 82 additions & 2 deletions

File tree

Sources/PreviewsCore/SPMBuildSystem.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,20 @@ public actor SPMBuildSystem: BuildSystem {
7171
consumerTargetName: targetName
7272
)
7373

74+
// 6b. Discover binary XCFramework dependencies.
75+
// SPM copies pre-built .framework bundles (from binaryTarget / XCFramework
76+
// dependencies) directly into binPath. These aren't covered by the
77+
// .build/-directory scan above — they need -F (framework search path)
78+
// and -framework flags instead of -L/-l.
79+
let frameworkNames = collectFrameworks(binPath: binPath)
80+
7481
// 7. Build compiler flags
7582
// -I <Modules> resolves dependency .swiftmodule files at compile time
7683
// -L <binPath> library search path for the archives created above
7784
// -l<Dep> per-dependency archive (lazy archive linking means only
7885
// object files actually referenced get pulled in)
86+
// -F <binPath> framework search path for binary XCFramework deps
87+
// -framework X link against a binary framework
7988
var flags: [String] = [
8089
"-I", modulesDir.path,
8190
]
@@ -85,6 +94,16 @@ public actor SPMBuildSystem: BuildSystem {
8594
flags += ["-l\(dep)"]
8695
}
8796
}
97+
if !frameworkNames.isEmpty {
98+
flags += ["-F", binPath.path]
99+
for fw in frameworkNames {
100+
flags += ["-framework", fw]
101+
}
102+
// Embed the framework search path as an rpath so dlopen can
103+
// find the framework at runtime (the dylib references it via
104+
// @rpath/Foo.framework/...).
105+
flags += ["-Xlinker", "-rpath", "-Xlinker", binPath.path]
106+
}
88107

89108
// Add C module include paths for targets with C shims
90109
let targetBuildDir = binPath.appendingPathComponent("\(targetName).build")
@@ -247,6 +266,34 @@ public actor SPMBuildSystem: BuildSystem {
247266
return libs
248267
}
249268

269+
/// Collect `.framework` bundles in binPath (binary XCFramework dependencies).
270+
/// Returns the framework names (e.g. ["Lottie"]) for use with `-framework`.
271+
private func collectFrameworks(binPath: URL) -> [String] {
272+
let fm = FileManager.default
273+
guard
274+
let entries = try? fm.contentsOfDirectory(
275+
at: binPath,
276+
includingPropertiesForKeys: [.isDirectoryKey],
277+
options: [.skipsHiddenFiles]
278+
)
279+
else {
280+
return []
281+
}
282+
283+
var frameworks: [String] = []
284+
for entry in entries {
285+
let name = entry.lastPathComponent
286+
guard name.hasSuffix(".framework") else { continue }
287+
var isDir: ObjCBool = false
288+
guard fm.fileExists(atPath: entry.path, isDirectory: &isDir), isDir.boolValue else {
289+
continue
290+
}
291+
let frameworkName = String(name.dropLast(".framework".count))
292+
frameworks.append(frameworkName)
293+
}
294+
return frameworks
295+
}
296+
250297
/// Recursively collect `.o` files under a target's build directory, including
251298
/// files named `Foo.swift.o` that swift build emits for Swift sources.
252299
private func collectObjectFiles(in directory: URL) -> [URL] {

Tests/PreviewsCoreTests/PreviewSessionBuildContextTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,27 @@ struct PreviewSessionBuildContextTests {
7373
!flags.contains("-lToDo"),
7474
"Consumer target should not be linked as a library"
7575
)
76+
// Binary XCFramework dependency (lottie-spm) — SPM copies .framework bundles
77+
// into binPath instead of producing .build/ directories with loose .o files.
78+
#expect(
79+
flags.contains("-F"),
80+
"SPMBuildSystem should add -F <binPath> for binary framework deps; flags were: \(flags)"
81+
)
82+
#expect(
83+
flags.contains("-framework"),
84+
"SPMBuildSystem should add -framework flags for binary deps; flags were: \(flags)"
85+
)
86+
// Verify the actual framework name is emitted, not just the flag.
87+
if let idx = flags.firstIndex(of: "-framework") {
88+
#expect(
89+
idx + 1 < flags.count && flags[idx + 1] == "Lottie",
90+
"Expected -framework Lottie; flags were: \(flags)"
91+
)
92+
}
93+
#expect(
94+
flags.contains("-rpath"),
95+
"SPMBuildSystem should add -rpath for framework dlopen; flags were: \(flags)"
96+
)
7697
}
7798

7899
@Test("Tier 2 compile: dylib + populated literals + DesignTimeStore symbols")

examples/spm/Package.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ let package = Package(
99
.library(name: "ToDoExtras", targets: ["ToDoExtras"]),
1010
],
1111
dependencies: [
12-
.package(path: "LocalDep")
12+
.package(path: "LocalDep"),
13+
.package(url: "https://github.com/airbnb/lottie-spm.git", from: "4.4.0"),
1314
],
1415
targets: [
1516
.target(
@@ -18,7 +19,11 @@ let package = Package(
1819
),
1920
.target(
2021
name: "ToDo",
21-
dependencies: ["ToDoExtras", .product(name: "LocalDep", package: "LocalDep")],
22+
dependencies: [
23+
"ToDoExtras",
24+
.product(name: "LocalDep", package: "LocalDep"),
25+
.product(name: "Lottie", package: "lottie-spm"),
26+
],
2227
path: "Sources/ToDo"
2328
),
2429
]

examples/spm/Sources/ToDo/ToDoView.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import Lottie
12
import SwiftUI
23

34
/// A view that references `Item` from Item.swift.
@@ -60,6 +61,12 @@ struct ToDoView: View {
6061
}
6162
}
6263
.navigationTitle("My Items")
64+
.overlay {
65+
if items.isEmpty {
66+
LottieView(animation: nil)
67+
.frame(width: 100, height: 100)
68+
}
69+
}
6370
}
6471
}
6572
}

0 commit comments

Comments
 (0)