Skip to content

Commit afe833c

Browse files
authored
Merge pull request #206 from S-Shimotori/issue/116
Set OtherLDFlags for dynamic framework type
2 parents 79118d9 + a30d70a commit afe833c

File tree

19 files changed

+311
-1
lines changed

19 files changed

+311
-1
lines changed

Sources/ScipioKit/Producer/PIF/PIFCompiler.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ struct PIFCompiler: Compiler {
6161
let generator = try PIFGenerator(
6262
packageName: descriptionPackage.name,
6363
packageLocator: descriptionPackage,
64+
allModules: descriptionPackage.graph.allModules,
6465
toolchainLibDirectory: buildParameters.toolchain.toolchainLibDir,
6566
buildOptions: buildOptions,
6667
buildOptionsMatrix: buildOptionsMatrix

Sources/ScipioKit/Producer/PIF/PIFGenerator.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import PIFKit
44
struct PIFGenerator {
55
private let packageName: String
66
private let packageLocator: any PackageLocator
7+
private let allModules: Set<ResolvedModule>
78
private let toolchainLibDirectory: Foundation.URL
89
private let buildOptions: BuildOptions
910
private let buildOptionsMatrix: [String: BuildOptions]
@@ -13,6 +14,7 @@ struct PIFGenerator {
1314
init(
1415
packageName: String,
1516
packageLocator: some PackageLocator,
17+
allModules: Set<ResolvedModule>,
1618
toolchainLibDirectory: Foundation.URL,
1719
buildOptions: BuildOptions,
1820
buildOptionsMatrix: [String: BuildOptions],
@@ -21,6 +23,7 @@ struct PIFGenerator {
2123
) throws {
2224
self.packageName = packageName
2325
self.packageLocator = packageLocator
26+
self.allModules = allModules
2427
self.toolchainLibDirectory = toolchainLibDirectory
2528
self.buildOptions = buildOptions
2629
self.buildOptionsMatrix = buildOptionsMatrix
@@ -124,9 +127,32 @@ struct PIFGenerator {
124127
}
125128
configuration.buildSettings["SWIFT_INSTALL_OBJC_HEADER"] = "YES"
126129

127-
if frameworkType == .mergeable {
130+
switch frameworkType {
131+
case .static:
132+
break
133+
case .mergeable:
128134
configuration.buildSettings["OTHER_LDFLAGS"]
129135
.append("-Wl,-make_mergeable")
136+
fallthrough
137+
case .dynamic:
138+
guard let resolvedTarget = allModules.first(where: { $0.c99name == target.c99Name }),
139+
let recursiveDependencies = try? resolvedTarget.recursiveDependencies() else {
140+
break
141+
}
142+
143+
let moduleDependenciesPerPlatforms = categorizeModuleDependenciesByPlatform(recursiveDependencies)
144+
145+
for (platforms, dependencies) in moduleDependenciesPerPlatforms {
146+
let flags = dependencies.flatMap {
147+
["-framework", $0.c99name]
148+
}
149+
150+
if let platforms {
151+
configuration.buildSettings["OTHER_LDFLAGS", for: platforms].append(flags)
152+
} else {
153+
configuration.buildSettings["OTHER_LDFLAGS"].append(flags)
154+
}
155+
}
130156
}
131157

132158
appendExtraFlagsByBuildOptionsMatrix(to: &configuration, target: target)
@@ -142,6 +168,34 @@ struct PIFGenerator {
142168
}
143169
}
144170

171+
private func categorizeModuleDependenciesByPlatform(
172+
_ dependencies: [ResolvedModule.Dependency]
173+
) -> [[PIFKit.Platform]?: [ResolvedModule]] {
174+
dependencies.reduce(into: [:]) { partialResult, dependency in
175+
guard case .module(let module, let conditions) = dependency else {
176+
return
177+
}
178+
if conditions.isEmpty {
179+
partialResult[nil, default: []].append(module)
180+
return
181+
}
182+
for condition in conditions {
183+
let platforms = condition.platformNames.compactMap {
184+
PIFKit.Platform(rawValue: $0)
185+
}
186+
partialResult[platforms, default: []].append(module)
187+
188+
if condition.config != nil {
189+
// FIXME: Handle config condition
190+
}
191+
192+
if condition.traits != nil {
193+
// FIXME: Handle trait condition
194+
}
195+
}
196+
}
197+
}
198+
145199
// Append extraFlags from BuildOptionsMatrix to each target settings
146200
private func appendExtraFlagsByBuildOptionsMatrix(to configuration: inout PIFKit.BuildConfiguration, target: PIFKit.Target) {
147201
func createOrUpdateFlags(for key: String, to keyPath: KeyPath<ExtraFlags, [String]?>) {
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import Foundation
2+
import Testing
3+
@testable @_spi(Internals) import ScipioKit
4+
5+
private let fixturePath = URL(filePath: #filePath)
6+
.deletingLastPathComponent()
7+
.appending(components: "Resources", "Fixtures")
8+
9+
@Suite(.serialized)
10+
struct DynamicFrameworkTests {
11+
private let fileManager: FileManager = .default
12+
13+
@Test(
14+
arguments: [FrameworkType.dynamic, .mergeable]
15+
)
16+
func otherLDFlags(frameworkType: FrameworkType) async throws {
17+
let packageName = "DynamicFrameworkOtherLDFlagsTestPackage"
18+
19+
let outputDir = fileManager.temporaryDirectory
20+
.appending(components: "Scipio", packageName)
21+
22+
defer {
23+
_ = try? FileManager.default.removeItem(atPath: outputDir.path)
24+
}
25+
26+
try await buildPackage(
27+
packageName: packageName,
28+
buildOptionsMatrix: [
29+
"UsableFromInlinePackage": .init(frameworkType: frameworkType),
30+
],
31+
outputDir: outputDir
32+
)
33+
34+
try await checkPlatformDependentLibraries(
35+
framework: .init(
36+
name: "UsableFromInlinePackage",
37+
destinations: [.iOS, .macOS]
38+
),
39+
dependencies: [
40+
"ClangModule": [.iOS, .macOS],
41+
"ClangModuleForIOS": [.iOS],
42+
"ClangModuleForMacOS": [.macOS],
43+
],
44+
outputDir: outputDir
45+
)
46+
}
47+
48+
private func buildPackage(
49+
packageName: String,
50+
buildOptionsMatrix: [String: Runner.Options.TargetBuildOptions],
51+
outputDir: URL
52+
) async throws {
53+
let runner = Runner(
54+
mode: .prepareDependencies,
55+
options: .init(
56+
baseBuildOptions: .init(
57+
buildConfiguration: .release,
58+
isSimulatorSupported: false,
59+
isDebugSymbolsEmbedded: false,
60+
frameworkType: .static,
61+
enableLibraryEvolution: false
62+
),
63+
buildOptionsMatrix: buildOptionsMatrix,
64+
shouldOnlyUseVersionsFromResolvedFile: true,
65+
cachePolicies: .disabled,
66+
overwrite: true,
67+
verbose: false
68+
)
69+
)
70+
let packageDir = fixturePath.appending(component: packageName)
71+
72+
try await runner.run(
73+
packageDirectory: packageDir,
74+
frameworkOutputDir: .custom(outputDir)
75+
)
76+
}
77+
78+
private func checkPlatformDependentLibraries(
79+
framework: Framework,
80+
dependencies: [String: Set<Destination>],
81+
outputDir: URL,
82+
sourceLocation: SourceLocation = #_sourceLocation
83+
) async throws {
84+
let xcFrameworkPath = outputDir.appending(component: framework.xcFrameworkName)
85+
86+
let executor = ProcessExecutor()
87+
88+
for destination in Destination.allCases {
89+
let binaryPath = xcFrameworkPath.appending(
90+
components: destination.rawValue, "\(framework.name).framework", framework.name
91+
)
92+
93+
let executionResult = try await executor.execute("/usr/bin/otool", "-l", binaryPath.path(percentEncoded: false))
94+
let loadCommands = try executionResult.unwrapOutput()
95+
96+
for (dependencyName, destinations) in dependencies {
97+
let shouldContain = destinations.contains(destination)
98+
#expect(
99+
loadCommands.contains(dependencyName) == shouldContain,
100+
"\(dependencyName) \(shouldContain ? "must" : "must NOT") be linked to \(framework.name) for \(destination)."
101+
)
102+
}
103+
}
104+
}
105+
106+
private struct Framework {
107+
var name: String
108+
var destinations: Set<Destination>
109+
110+
var xcFrameworkName: String {
111+
"\(name).xcframework"
112+
}
113+
}
114+
115+
private enum Destination: String, CaseIterable {
116+
case iOS = "ios-arm64"
117+
case macOS = "macos-arm64_x86_64"
118+
}
119+
}

Tests/ScipioKitTests/IntegrationTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,21 @@ final class IntegrationTests: XCTestCase {
9595
)
9696
}
9797

98+
func testDynamicFramework() async throws {
99+
try await testBuildPackages(
100+
packageName: "DynamicFrameworkOtherLDFlagsTestPackage",
101+
buildOptionsMatrix: [
102+
"UsableFromInlinePackage": .init(frameworkType: .dynamic),
103+
],
104+
testCases: [
105+
("UsableFromInlinePackage", .dynamic, [.iOS, .macOS], false),
106+
("ClangModule", .static, [.iOS, .macOS], true),
107+
("ClangModuleForIOS", .static, [.iOS, .macOS], true),
108+
("ClangModuleForMacOS", .static, [.iOS, .macOS], true),
109+
]
110+
)
111+
}
112+
98113
private typealias TestCase = (String, FrameworkType, Set<Destination>, Bool)
99114

100115
private func testBuildPackages(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "DynamicFrameworkOtherLDFlagsTestPackage",
7+
platforms: [.iOS(.v18), .macOS(.v10_15)],
8+
products: [
9+
.library(name: "DynamicFrameworkOtherLDFlagsTestPackage", targets: ["DynamicFrameworkOtherLDFlagsTestPackage"])
10+
],
11+
dependencies: [
12+
.package(name: "UsableFromInlinePackage", path: "../UsableFromInlinePackage")
13+
],
14+
targets: [
15+
.target(
16+
name: "DynamicFrameworkOtherLDFlagsTestPackage",
17+
dependencies: [
18+
.product(name: "UsableFromInlinePackage", package: "UsableFromInlinePackage")
19+
]),
20+
]
21+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
xcuserdata/
5+
DerivedData/
6+
.swiftpm/configuration/registries.json
7+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8+
.netrc
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version: 6.0
2+
3+
import PackageDescription
4+
5+
let package = Package(
6+
name: "UsableFromInlinePackage",
7+
products: [
8+
.library(
9+
name: "UsableFromInlinePackage",
10+
targets: ["UsableFromInlinePackage"]),
11+
],
12+
targets: [
13+
.target(
14+
name: "ClangModule"
15+
),
16+
.target(
17+
name: "ClangModuleForIOS"
18+
),
19+
.target(
20+
name: "ClangModuleForMacOS"
21+
),
22+
.target(
23+
name: "UsableFromInlinePackage",
24+
dependencies: [
25+
"ClangModule",
26+
.targetItem(name: "ClangModuleForIOS", condition: .when(platforms: [.iOS])),
27+
.targetItem(name: "ClangModuleForMacOS", condition: .when(platforms: [.macOS])),
28+
]
29+
),
30+
]
31+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
void this_is_c_function();

0 commit comments

Comments
 (0)