From 428855d3d14afb19d5120ecc1691ee2c057c0845 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Tue, 9 Dec 2025 18:20:42 -0800 Subject: [PATCH 1/2] Correctly specify platform_version when prelinking zippered binaries --- .../Tools/PrelinkedObjectLink.swift | 22 ++- Sources/SWBUtil/MachO.swift | 2 +- .../PrelinkedObjectFileTests.swift | 143 ++++++++++++++++++ 3 files changed, 161 insertions(+), 6 deletions(-) diff --git a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift index d2e17180..51186d39 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift @@ -35,11 +35,23 @@ public final class PrelinkedObjectLinkSpec: CommandLineToolSpec, SpecImplementat var commandLine = [toolSpecInfo.toolPath.str] commandLine += ["-r", "-arch", arch] - if let buildPlatform = cbc.producer.sdk?.targetBuildVersionPlatform(sdkVariant: cbc.producer.sdkVariant), - let deploymentTargetMacro = cbc.producer.platform?.deploymentTargetMacro, - let minDeploymentTarget = cbc.scope.evaluate(deploymentTargetMacro).nilIfEmpty, - let sdkVersion = cbc.producer.sdk?.version { - commandLine += ["-platform_version", "\(buildPlatform.rawValue)", minDeploymentTarget, sdkVersion.canonicalDeploymentTargetForm.description] + if let sdk = cbc.producer.sdk, let sdkVersion = sdk.version { + for buildPlatform in cbc.producer.targetBuildVersionPlatforms(in: cbc.scope)?.sorted() ?? [] { + let deploymentTargetSettingName = buildPlatform.deploymentTargetSettingName(infoLookup: cbc.producer) + if let minDeploymentTarget = cbc.scope.evaluate(cbc.scope.namespace.parseString("$(\(deploymentTargetSettingName)")).nilIfEmpty { + let version: Version + if cbc.scope.evaluate(BuiltinMacros.IS_ZIPPERED) && buildPlatform == .macCatalyst { + guard let correspondingVersion = sdk.versionMap["macOS_iOSMac"]?[sdkVersion] else { + delegate.error("'\(sdk.canonicalName)' us missing a Mac Catalyst version mapping for '\(sdkVersion)'") + continue + } + version = correspondingVersion + } else { + version = sdkVersion + } + commandLine += ["-platform_version", "\(buildPlatform.rawValue)", minDeploymentTarget, version.canonicalDeploymentTargetForm.description] + } + } } // We do not pass the deployment target to the linker here. Instead the linker infers the platform and deployment target from the .o files being collected. We did briefly pass it to the linker to silence a linker warning - if we ever see issues here we should confer with the linker folks to make sure we do the right thing. See for more about the history here. diff --git a/Sources/SWBUtil/MachO.swift b/Sources/SWBUtil/MachO.swift index 275394f8..6ba06dc3 100644 --- a/Sources/SWBUtil/MachO.swift +++ b/Sources/SWBUtil/MachO.swift @@ -81,7 +81,7 @@ public protocol PlatformInfoProvider { public struct BuildVersion: Equatable, Hashable, Sendable { /// Enumerates platforms as defined by dyld. /// - public struct Platform: RawRepresentable, Equatable, Hashable, Serializable, Sendable { + public struct Platform: RawRepresentable, Equatable, Hashable, Serializable, Sendable, Comparable { public typealias RawValue = UInt32 public static let macOS = Self(platformID: 1) diff --git a/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift b/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift index 82dfc726..64c57785 100644 --- a/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift +++ b/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift @@ -221,6 +221,149 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests { } } + @Test(.requireSDKs(.macOS, .iOS)) + func prelinkedObjectFileGenerationVariant_zippered() async throws { + let core = try await getCore() + // Test that a prelinked object file gets generated even if the target contains no sources or libraries in its build phases. + let testProject = TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "MACOSX_DEPLOYMENT_TARGET": core.loadSDK(.macOS).defaultDeploymentTarget, + "IPHONEOS_DEPLOYMENT_TARGET": core.loadSDK(.iOS).defaultDeploymentTarget, + "SDKROOT": "macosx", + "IS_ZIPPERED": "YES", + ]), + ], + targets: [ + TestStandardTarget( + "AllLibraries", + type: .staticLibrary, + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_PRELINK_OBJECT_FILE": "YES", + ]), + ], + buildPhases: [ + TestSourcesBuildPhase([]), + TestFrameworksBuildPhase([]), + ], + dependencies: ["Tool"]), + ]) + let testWorkspace = TestWorkspace("aWorkspace", projects: [testProject]) + let tester = try TaskConstructionTester(core, testWorkspace) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + // Check a build for MacCatalyst. + await tester.checkBuild(runDestination: .macCatalyst, fs: localFS) { results in + // Ignore all tasks we don't want to check. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("RegisterExecutionPolicyException")) { _ in } + + results.checkTarget("AllLibraries") { target in + // There should be tasks to create the prelinked object file and then the static library. + results.checkTask(.matchTarget(target), .matchRuleType("PrelinkedObjectLink")) { task in + task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", .equal(results.runDestinationTargetArchitecture), "-platform_version", "1", .any, .any, "-platform_version", "6", .any, .any, "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/libAllLibraries.a-\(results.runDestinationTargetArchitecture)-prelink.o")]) + } + results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in + task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", .equal(results.runDestinationTargetArchitecture), "-D", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("-L\(SRCROOT)/build/Debug"), "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/\(results.runDestinationTargetArchitecture)/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/\(results.runDestinationTargetArchitecture)/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug/libAllLibraries.a")]) + } + } + + // There should be no other tasks. + results.checkNoTask() + + // There shouldn't be any diagnostics. + results.checkNoDiagnostics() + + // Check there are no other targets. + #expect(results.otherTargets == []) + } + } + + @Test(.requireSDKs(.macOS, .iOS)) + func prelinkedObjectFileGenerationVariant_reverseZippered() async throws { + let core = try await getCore() + // Test that a prelinked object file gets generated even if the target contains no sources or libraries in its build phases. + let testProject = TestProject( + "aProject", + groupTree: TestGroup( + "SomeFiles", path: "Sources", + children: [ + ]), + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "PRODUCT_NAME": "$(TARGET_NAME)", + "MACOSX_DEPLOYMENT_TARGET": core.loadSDK(.macOS).defaultDeploymentTarget, + "IPHONEOS_DEPLOYMENT_TARGET": core.loadSDK(.iOS).defaultDeploymentTarget, + "SDKROOT": "macosx", + "IS_ZIPPERED": "YES", + "SDK_VARIANT": MacCatalystInfo.sdkVariantName, + ]), + ], + targets: [ + TestStandardTarget( + "AllLibraries", + type: .staticLibrary, + buildConfigurations: [ + TestBuildConfiguration( + "Debug", + buildSettings: [ + "GENERATE_PRELINK_OBJECT_FILE": "YES", + ]), + ], + buildPhases: [ + TestSourcesBuildPhase([]), + TestFrameworksBuildPhase([]), + ], + dependencies: ["Tool"]), + ]) + let testWorkspace = TestWorkspace("aWorkspace", projects: [testProject]) + let tester = try TaskConstructionTester(core, testWorkspace) + let SRCROOT = tester.workspace.projects[0].sourceRoot.str + + // Check a build for MacCatalyst. + await tester.checkBuild(runDestination: .macCatalyst, fs: localFS) { results in + // Ignore all tasks we don't want to check. + results.checkTasks(.matchRuleType("Gate")) { _ in } + results.checkTasks(.matchRuleType("WriteAuxiliaryFile")) { _ in } + results.checkTasks(.matchRuleType("CreateBuildDirectory")) { _ in } + results.checkTasks(.matchRuleType("RegisterExecutionPolicyException")) { _ in } + + results.checkTarget("AllLibraries") { target in + // There should be tasks to create the prelinked object file and then the static library. + results.checkTask(.matchTarget(target), .matchRuleType("PrelinkedObjectLink")) { task in + task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", .equal(results.runDestinationTargetArchitecture), "-platform_version", "1", .any, .any, "-platform_version", "6", .any, .any, "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-maccatalyst/AllLibraries.build/Objects-normal/libAllLibraries.a-\(results.runDestinationTargetArchitecture)-prelink.o")]) + } + results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in + task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", .equal(results.runDestinationTargetArchitecture), "-D", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("-L\(SRCROOT)/build/Debug-maccatalyst"), "-L\(core.loadSDK(.macOS).path.str)/System/iOSSupport/usr/lib", "-L\(core.developerPath.path.str)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/maccatalyst", "-L\(core.loadSDK(.macOS).path.str)/System/iOSSupport/usr/lib", "-L\(core.developerPath.path.str)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/maccatalyst", "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug-maccatalyst/AllLibraries.build/Objects-normal/\(results.runDestinationTargetArchitecture)/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug-maccatalyst/AllLibraries.build/Objects-normal/\(results.runDestinationTargetArchitecture)/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug-maccatalyst/libAllLibraries.a")]) + } + } + + // There should be no other tasks. + results.checkNoTask() + + // There shouldn't be any diagnostics. + results.checkNoDiagnostics() + + // Check there are no other targets. + #expect(results.otherTargets == []) + } + } + @Test(.requireSDKs(.iOS)) func prelinkedObjectFileGenerationVariant_ios() async throws { let core = try await getCore() From f4aa1395945cd7a306663bedde2cfcac70e38429 Mon Sep 17 00:00:00 2001 From: Owen Voorhees Date: Thu, 11 Dec 2025 10:00:08 -0800 Subject: [PATCH 2/2] Update Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift Co-authored-by: Jake Petroules --- .../SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift index 51186d39..f112e35c 100644 --- a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift +++ b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift @@ -42,7 +42,7 @@ public final class PrelinkedObjectLinkSpec: CommandLineToolSpec, SpecImplementat let version: Version if cbc.scope.evaluate(BuiltinMacros.IS_ZIPPERED) && buildPlatform == .macCatalyst { guard let correspondingVersion = sdk.versionMap["macOS_iOSMac"]?[sdkVersion] else { - delegate.error("'\(sdk.canonicalName)' us missing a Mac Catalyst version mapping for '\(sdkVersion)'") + delegate.error("'\(sdk.canonicalName)' is missing a Mac Catalyst version mapping for '\(sdkVersion)'") continue } version = correspondingVersion