From fb5ef989cef81127c679fa9be021a4bb752226f6 Mon Sep 17 00:00:00 2001 From: Dalton Claybrook Date: Thu, 23 Jul 2026 14:28:19 -0400 Subject: [PATCH] Fix static linking issue with SPM packages --- Sources/ProjectSpec/XCProjExtensions.swift | 4 ++ Sources/XcodeGenKit/PBXProjGenerator.swift | 7 ++- .../ProjectGeneratorTests.swift | 57 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/Sources/ProjectSpec/XCProjExtensions.swift b/Sources/ProjectSpec/XCProjExtensions.swift index ba8e2068..48b17f99 100644 --- a/Sources/ProjectSpec/XCProjExtensions.swift +++ b/Sources/ProjectSpec/XCProjExtensions.swift @@ -22,6 +22,10 @@ extension PBXProductType { self == .staticLibrary || self == .dynamicLibrary } + public var isStatic: Bool { + self == .staticLibrary || self == .staticFramework + } + public var isExtension: Bool { fileExtension == "appex" } diff --git a/Sources/XcodeGenKit/PBXProjGenerator.swift b/Sources/XcodeGenKit/PBXProjGenerator.swift index 604ff473..267e52bc 100644 --- a/Sources/XcodeGenKit/PBXProjGenerator.swift +++ b/Sources/XcodeGenKit/PBXProjGenerator.swift @@ -964,12 +964,13 @@ public class PBXProjGenerator { XCSwiftPackageProductDependency(productName: productName, package: packageReference) ) - // Add package dependency if linking is true. - if dependency.link ?? true { + // Xcode bakes packageProductDependencies into a static framework's + // object code, so exclude them to avoid duplicate symbols at link time. + if dependency.link ?? (target.type != .staticFramework) { packageDependencies.append(packageDependency) } - let link = dependency.link ?? (target.type != .staticLibrary) + let link = dependency.link ?? !target.type.isStatic if link { let file = PBXBuildFile(product: packageDependency, settings: getDependencyFrameworkSettings(dependency: dependency)) file.platformFilter = platform diff --git a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift index dd07d770..5ba02a16 100644 --- a/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift +++ b/Tests/XcodeGenKitTests/ProjectGeneratorTests.swift @@ -1956,6 +1956,63 @@ class ProjectGeneratorTests: XCTestCase { try expect(productNames).contains { $0 == "FooDomain" } try expect(productNames).contains { $0 == "FooUI" } } + + $0.it("does not link packages in static framework targets") { + let staticFramework = Target( + name: "MyStaticFramework", + type: .staticFramework, + platform: .iOS, + dependencies: [ + Dependency(type: .package(products: ["RxSwift"]), reference: "RxSwift"), + ] + ) + + let project = Project(name: "test", targets: [staticFramework], packages: [ + "RxSwift": .remote(url: "http://github.com/ReactiveX/RxSwift", versionRequirement: .exact("6.0.0")), + ]) + + let pbxProject = try project.generatePbxProj(specValidate: false) + let nativeTarget = try unwrap(pbxProject.nativeTargets.first(where: { $0.name == staticFramework.name })) + + // Package should not appear in packageProductDependencies + try expect(nativeTarget.packageProductDependencies?.isEmpty ?? true) == true + + // Package should not appear in the Frameworks build phase + let frameworkPhases = nativeTarget.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase } + let linkedPackages = (frameworkPhases.first?.files ?? []).compactMap { $0.product?.productName } + try expect(linkedPackages.isEmpty) == true + + // Package should appear as a build-order-only target dependency + let depProductNames = nativeTarget.dependencies.compactMap { $0.product?.productName } + try expect(depProductNames) == ["RxSwift"] + } + + $0.it("links packages in static library targets' packageProductDependencies") { + let staticLibrary = Target( + name: "MyStaticLibrary", + type: .staticLibrary, + platform: .iOS, + dependencies: [ + Dependency(type: .package(products: ["RxSwift"]), reference: "RxSwift"), + ] + ) + + let project = Project(name: "test", targets: [staticLibrary], packages: [ + "RxSwift": .remote(url: "http://github.com/ReactiveX/RxSwift", versionRequirement: .exact("6.0.0")), + ]) + + let pbxProject = try project.generatePbxProj(specValidate: false) + let nativeTarget = try unwrap(pbxProject.nativeTargets.first(where: { $0.name == staticLibrary.name })) + + // Package should appear in packageProductDependencies (module is importable) + let packageDepNames = nativeTarget.packageProductDependencies?.map(\.productName) ?? [] + try expect(packageDepNames) == ["RxSwift"] + + // Package should not appear in the Frameworks build phase + let frameworkPhases = nativeTarget.buildPhases.compactMap { $0 as? PBXFrameworksBuildPhase } + let linkedPackages = (frameworkPhases.first?.files ?? []).compactMap { $0.product?.productName } + try expect(linkedPackages.isEmpty) == true + } } }