From da06329026c6a708176f45c7f5689d81f09f8a61 Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Wed, 9 Jul 2025 20:38:18 -0700
Subject: [PATCH 01/49] Update metal tests to account for a downloaded metal
 toolchain

rdar://146431505
---
 Sources/SWBTestSupport/CoreTestSupport.swift  | 34 +++++++++++++++++++
 .../BuildCommandTests.swift                   |  4 ++-
 .../BuildOperationTests.swift                 |  4 ++-
 3 files changed, 40 insertions(+), 2 deletions(-)

diff --git a/Sources/SWBTestSupport/CoreTestSupport.swift b/Sources/SWBTestSupport/CoreTestSupport.swift
index e95da70f..1bf280ea 100644
--- a/Sources/SWBTestSupport/CoreTestSupport.swift
+++ b/Sources/SWBTestSupport/CoreTestSupport.swift
@@ -70,6 +70,40 @@ extension Core {
             try POSIX.unsetenv(variable)
         }
 
+        // Handle Xcodes with an unbundled Metal toolchain by querying `xcodebuild` if needed.
+        //
+        // If the given environment already contains `EXTERNAL_TOOLCHAINS_DIR` and `TOOLCHAINS`, we're assuming that we do not have to obtain any toolchain information.
+        var environment = environment
+        if (try? ProcessInfo.processInfo.hostOperatingSystem()) == .macOS, !(environment.contains("EXTERNAL_TOOLCHAINS_DIR") && environment.contains("TOOLCHAINS")) {
+            let activeDeveloperPath: Path
+            if let developerPath {
+                activeDeveloperPath = developerPath.path
+            } else {
+                activeDeveloperPath = try await Xcode.getActiveDeveloperDirectoryPath()
+            }
+            let defaultToolchainPath = activeDeveloperPath.join("Toolchains/XcodeDefault.xctoolchain")
+
+            if !localFS.exists(defaultToolchainPath.join("usr/metal/current")) {
+                struct MetalToolchainInfo: Decodable {
+                    let buildVersion: String
+                    let status: String
+                    let toolchainIdentifier: String
+                    let toolchainSearchPath: String
+                }
+
+                let result = try await Process.getOutput(url: URL(fileURLWithPath: activeDeveloperPath.join("usr/bin/xcodebuild").str), arguments: ["-showComponent", "metalToolchain", "-json"], environment: ["DEVELOPER_DIR": activeDeveloperPath.str])
+                if result.exitStatus != .exit(0) {
+                    throw StubError.error("xcodebuild failed: \(String(data: result.stdout, encoding: .utf8) ?? "")\n\(String(data: result.stderr, encoding: .utf8) ?? "")")
+                }
+
+                let metalToolchainInfo = try JSONDecoder().decode(MetalToolchainInfo.self, from: result.stdout)
+                environment.addContents(of: [
+                    "TOOLCHAINS": "\(metalToolchainInfo.toolchainIdentifier) $(inherited)",
+                    "EXTERNAL_TOOLCHAINS_DIR": metalToolchainInfo.toolchainSearchPath,
+                ])
+            }
+        }
+
         // When this code is being loaded directly via unit tests *and* we detect the products directory we are running in is for Xcode, then we should run using inferior search paths.
         let inferiorProductsPath: Path? = self.inferiorProductsPath()
 
diff --git a/Tests/SWBBuildSystemTests/BuildCommandTests.swift b/Tests/SWBBuildSystemTests/BuildCommandTests.swift
index 2cd6a109..c2aa61af 100644
--- a/Tests/SWBBuildSystemTests/BuildCommandTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildCommandTests.swift
@@ -325,6 +325,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests {
 
     @Test(.requireSDKs(.macOS), .requireXcode16())
     func singleFileCompileMetal() async throws {
+        let core = try await getCore()
         try await withTemporaryDirectory { tmpDirPath async throws -> Void in
             let testWorkspace = try await TestWorkspace(
                 "Test",
@@ -337,6 +338,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests {
                             "Debug",
                             buildSettings: ["PRODUCT_NAME": "$(TARGET_NAME)",
                                             "SWIFT_ENABLE_EXPLICIT_MODULES": "NO",
+                                            "TOOLCHAINS": core.environment["TOOLCHAINS"] ?? "$(inherited)",
                                             "SWIFT_VERSION": swiftVersion])],
                         targets: [
                             TestStandardTarget(
@@ -348,7 +350,7 @@ fileprivate struct BuildCommandTests: CoreBasedTests {
                     )
                 ]
             )
-            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+            let tester = try await BuildOperationTester(core, testWorkspace, simulated: false)
 
             let metalFile = testWorkspace.sourceRoot.join("aProject/Metal.metal")
             try await tester.fs.writeFileContents(metalFile) { stream in }
diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
index 61624fd0..3d5f3dde 100644
--- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
@@ -5756,6 +5756,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
 
     @Test(.requireSDKs(.macOS))
     func incrementalMetalLinkWithCodeSign() async throws {
+        let core = try await getCore()
         try await withTemporaryDirectory { tmpDirPath async throws -> Void in
             let testWorkspace = try await TestWorkspace(
                 "Test",
@@ -5773,6 +5774,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                                             "CODE_SIGN_IDENTITY": "-",
                                             "INFOPLIST_FILE": "Info.plist",
                                             "CODESIGN": "/usr/bin/true",
+                                            "TOOLCHAINS": core.environment["TOOLCHAINS"] ?? "$(inherited)",
                                             "SWIFT_VERSION": swiftVersion])],
                         targets: [
                             TestStandardTarget(
@@ -5781,7 +5783,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                                 buildPhases: [
                                     TestSourcesBuildPhase(["SwiftFile.swift", "Metal.metal"]),
                                 ])])])
-            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false, fileSystem: localFS)
+            let tester = try await BuildOperationTester(core, testWorkspace, simulated: false, fileSystem: localFS)
             let signableTargets: Set<String> = ["aFramework"]
 
             // Create the input files.

From a1ef8550750249bd802a1c569aa481be70874eb1 Mon Sep 17 00:00:00 2001
From: Deniz Dizman <ddizman@apple.com>
Date: Thu, 10 Jul 2025 17:29:47 -0700
Subject: [PATCH 02/49] Upstream: add LM_ENABLE_APP_NAME_OVERRIDE option to
 AppIntentsMetadata spec

---
 .../Specs/AppIntentsMetadata.xcspec           |   9 ++
 ...IntentsMetadataTaskConstructionTests.swift | 127 ++++++++++++++++++
 2 files changed, 136 insertions(+)

diff --git a/Sources/SWBApplePlatform/Specs/AppIntentsMetadata.xcspec b/Sources/SWBApplePlatform/Specs/AppIntentsMetadata.xcspec
index 91487be6..60791556 100644
--- a/Sources/SWBApplePlatform/Specs/AppIntentsMetadata.xcspec
+++ b/Sources/SWBApplePlatform/Specs/AppIntentsMetadata.xcspec
@@ -203,6 +203,15 @@
                     NO = ();
                 };
             },
+            {
+                Name = LM_ENABLE_APP_NAME_OVERRIDE;
+                Type = Boolean;
+                DefaultValue = NO;
+                CommandLineArgs = {
+                    YES = ( "--app-shortcuts-app-name-override" );
+                    NO = ();
+                };
+            },
         );
     },
 )
diff --git a/Tests/SWBTaskConstructionTests/AppIntentsMetadataTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/AppIntentsMetadataTaskConstructionTests.swift
index afd64f3c..cacf5453 100644
--- a/Tests/SWBTaskConstructionTests/AppIntentsMetadataTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/AppIntentsMetadataTaskConstructionTests.swift
@@ -1595,6 +1595,133 @@ fileprivate struct AppIntentsMetadataTaskConstructionTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.iOS))
+    func appNameOverrideIsNotPresent() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = try await TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("source.swift"),
+                        TestFile(appShortcutsStringsFileName)
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "AD_HOC_CODE_SIGNING_ALLOWED": "YES",
+                            "ARCHS": "arm64",
+                            "CODE_SIGN_IDENTITY": "-",
+                            "GENERATE_INFOPLIST_FILE": "YES",
+                            "PRODUCT_BUNDLE_IDENTIFIER": "com.foo.bar",
+                            "PRODUCT_NAME": "$(TARGET_NAME)",
+                            "SDKROOT": "iphoneos",
+                            "SWIFT_EXEC": swiftCompilerPath.str,
+                            "SWIFT_VERSION": swiftVersion,
+                            "VERSIONING_SYSTEM": "apple-generic",
+                            "SWIFT_EMIT_CONST_VALUE_PROTOCOLS": "Foo Bar",
+                        ]),
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "LinkTest",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "LM_ENABLE_LINK_GENERATION": "YES",
+                                    "LM_ENABLE_APP_NAME_OVERRIDE": "NO"
+                                ]),
+                        ],
+                        buildPhases: [
+                            TestResourcesBuildPhase([TestBuildFile(appShortcutsStringsFileName)]),
+                            TestSourcesBuildPhase(["source.swift"]),
+                        ]
+                    )
+                ])
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            await tester.checkBuild(runDestination: .iOS) { results in
+                results.checkTask(.matchRuleType("ExtractAppIntentsMetadata")) { task in
+                    let executableName = task.commandLine.first
+                    if let executableName,
+                       executableName == "appintentsmetadataprocessor" {
+                        task.checkCommandLineDoesNotContain("--app-shortcuts-app-name-override")
+                    }
+                    results.checkNoDiagnostics()
+                }
+            }
+        }
+    }
+
+
+    @Test(.requireSDKs(.iOS))
+    func appNameOverride() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = try await TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("source.swift"),
+                        TestFile(appShortcutsStringsFileName)
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "AD_HOC_CODE_SIGNING_ALLOWED": "YES",
+                            "ARCHS": "arm64",
+                            "CODE_SIGN_IDENTITY": "-",
+                            "GENERATE_INFOPLIST_FILE": "YES",
+                            "PRODUCT_BUNDLE_IDENTIFIER": "com.foo.bar",
+                            "PRODUCT_NAME": "$(TARGET_NAME)",
+                            "SDKROOT": "iphoneos",
+                            "SWIFT_EXEC": swiftCompilerPath.str,
+                            "SWIFT_VERSION": swiftVersion,
+                            "VERSIONING_SYSTEM": "apple-generic",
+                            "SWIFT_EMIT_CONST_VALUE_PROTOCOLS": "Foo Bar",
+                        ]),
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "LinkTest",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "LM_ENABLE_LINK_GENERATION": "YES",
+                                    "LM_ENABLE_APP_NAME_OVERRIDE": "YES"
+                                ]),
+                        ],
+                        buildPhases: [
+                            TestResourcesBuildPhase([TestBuildFile(appShortcutsStringsFileName)]),
+                            TestSourcesBuildPhase(["source.swift"]),
+                        ]
+                    )
+                ])
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            await tester.checkBuild(runDestination: .iOS) { results in
+                results.checkTask(.matchRuleType("ExtractAppIntentsMetadata")) { task in
+                    let executableName = task.commandLine.first
+                    if let executableName,
+                       executableName == "appintentsmetadataprocessor" {
+                        task.checkCommandLineContains(["--app-shortcuts-app-name-override"])
+                    }
+                    results.checkNoDiagnostics()
+                }
+            }
+        }
+    }
+
     @Test(.requireSDKs(.iOS))
     func quietWarnings() async throws {
         try await withTemporaryDirectory { tmpDir in

From 30321382a3652a4b9469d2804834f85e48e82224 Mon Sep 17 00:00:00 2001
From: Alex Carter <alexcarter@apple.com>
Date: Thu, 10 Jul 2025 17:46:02 -0700
Subject: [PATCH 03/49] Upstream: Extract Extension Point Definition and
 Extension's target extension point identifier from swift const values

---
 Sources/SWBApplePlatform/CMakeLists.txt       |   3 +
 Sources/SWBApplePlatform/EXUtil.swift         | 124 +++++++++++++
 .../ExtensionPointExtractorTaskProducer.swift | 171 ++++++++++++++++++
 Sources/SWBApplePlatform/Plugin.swift         |  37 +++-
 Sources/SWBApplePlatform/Specs/EXUtil.xcspec  |  42 +++++
 Sources/SWBCore/Settings/BuiltinMacros.swift  |   4 +
 .../FilesBasedBuildPhaseTaskProducer.swift    |   2 +-
 .../TaskProducers/TaskProducer.swift          |   2 +-
 .../SWBUniversalPlatform/Specs/Swift.xcspec   |   1 -
 .../EXUtilTaskConstructionTests.swift         | 155 ++++++++++++++++
 .../AppExtensionTaskConstructionTests.swift   |  93 ++++++++++
 11 files changed, 626 insertions(+), 8 deletions(-)
 create mode 100644 Sources/SWBApplePlatform/EXUtil.swift
 create mode 100644 Sources/SWBApplePlatform/ExtensionPointExtractorTaskProducer.swift
 create mode 100644 Sources/SWBApplePlatform/Specs/EXUtil.xcspec
 create mode 100644 Tests/SWBApplePlatformTests/EXUtilTaskConstructionTests.swift

diff --git a/Sources/SWBApplePlatform/CMakeLists.txt b/Sources/SWBApplePlatform/CMakeLists.txt
index e82db7a2..7dd6a466 100644
--- a/Sources/SWBApplePlatform/CMakeLists.txt
+++ b/Sources/SWBApplePlatform/CMakeLists.txt
@@ -17,7 +17,9 @@ add_library(SWBApplePlatform
   CoreDataCompiler.swift
   CoreMLCompiler.swift
   DittoTool.swift
+  ExtensionPointExtractorTaskProducer.swift
   ExtensionPointsCompiler.swift
+  EXUtil.swift
   IIGCompiler.swift
   InstrumentsPackageBuilderSpec.swift
   IntentsCompiler.swift
@@ -63,6 +65,7 @@ SwiftBuild_Bundle(MODULE SWBApplePlatform FILES
   Specs/Embedded-Shared.xcspec
   Specs/Embedded-Simulator.xcspec
   Specs/EmbeddedBinaryValidationUtility.xcspec
+  Specs/EXUtil.xcspec
   Specs/GenerateAppPlaygroundAssetCatalog.xcspec
   Specs/GenerateTextureAtlas.xcspec
   Specs/IBCompiler.xcspec
diff --git a/Sources/SWBApplePlatform/EXUtil.swift b/Sources/SWBApplePlatform/EXUtil.swift
new file mode 100644
index 00000000..d1a82063
--- /dev/null
+++ b/Sources/SWBApplePlatform/EXUtil.swift
@@ -0,0 +1,124 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import SWBUtil
+import SWBMacro
+import SWBCore
+import SWBProtocol
+import Foundation
+
+final class ExtensionPointExtractorSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable {
+    public static let identifier = "com.apple.compilers.extract-appextensionpoints"
+
+    static func shouldConstructTask(scope: MacroEvaluationScope, productType: ProductTypeSpec?, isApplePlatform: Bool) -> Bool {
+        let isNormalVariant = scope.evaluate(BuiltinMacros.CURRENT_VARIANT) == "normal"
+        let buildComponents = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS)
+        let isBuild = buildComponents.contains("build")
+        let indexEnableBuildArena = scope.evaluate(BuiltinMacros.INDEX_ENABLE_BUILD_ARENA)
+        let isAppProductType = productType?.conformsTo(identifier: "com.apple.product-type.application") ?? false
+        let extensionPointExtractorEnabled = scope.evaluate(BuiltinMacros.EX_ENABLE_EXTENSION_POINT_GENERATION)
+
+        let result = (
+            isBuild
+            && isNormalVariant
+            && extensionPointExtractorEnabled
+            && !indexEnableBuildArena
+            && isAppProductType
+            && isApplePlatform
+        )
+        return result
+    }
+
+    override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
+        guard Self.shouldConstructTask(scope: cbc.scope, productType: cbc.producer.productType, isApplePlatform: cbc.producer.isApplePlatform) else {
+            return
+        }
+
+        let inputs = cbc.inputs.map { input in
+            return delegate.createNode(input.absolutePath)
+        }.filter { node in
+            node.path.fileExtension == "swiftconstvalues" 
+        }
+        var outputs = [any PlannedNode]()
+
+        let outputPath = cbc.scope.evaluate(BuiltinMacros.EXTENSIONS_FOLDER_PATH).join(Path("\(cbc.scope.evaluate(BuiltinMacros.PRODUCT_MODULE_NAME))-generated.appexpt"))
+        outputs.append(delegate.createNode(outputPath))
+
+        let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString)
+
+        delegate.createTask(type: self,
+                            ruleInfo: defaultRuleInfo(cbc, delegate),
+                            commandLine: commandLine,
+                            environment: environmentFromSpec(cbc, delegate),
+                            workingDirectory: cbc.producer.defaultWorkingDirectory,
+                            inputs: inputs,
+                            outputs: outputs,
+                            action: nil,
+                            execDescription: resolveExecutionDescription(cbc, delegate),
+                            enableSandboxing: enableSandboxing)
+    }
+}
+
+final class AppExtensionPlistGeneratorSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable {
+    public static let identifier = "com.apple.compilers.appextension-plist-generator"
+
+    static func shouldConstructTask(scope: MacroEvaluationScope, productType: ProductTypeSpec?, isApplePlatform: Bool) -> Bool {
+        let isNormalVariant = scope.evaluate(BuiltinMacros.CURRENT_VARIANT) == "normal"
+        let buildComponents = scope.evaluate(BuiltinMacros.BUILD_COMPONENTS)
+        let isBuild = buildComponents.contains("build")
+        let indexEnableBuildArena = scope.evaluate(BuiltinMacros.INDEX_ENABLE_BUILD_ARENA)
+        let isAppExtensionProductType = productType?.conformsTo(identifier: "com.apple.product-type.extensionkit-extension") ?? false
+        let extensionPointAttributesGenerationEnabled = !scope.evaluate(BuiltinMacros.EX_DISABLE_APPEXTENSION_ATTRIBUTES_GENERATION)
+
+        let result = ( isBuild
+                       && isNormalVariant
+                       && extensionPointAttributesGenerationEnabled
+                       && !indexEnableBuildArena
+                       && (isAppExtensionProductType)
+                       && isApplePlatform )
+
+        return result
+    }
+
+    override func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
+        let scope = cbc.scope
+        let productType = cbc.producer.productType
+        let isApplePlatform = cbc.producer.isApplePlatform
+        guard Self.shouldConstructTask(scope: scope, productType: productType, isApplePlatform: isApplePlatform) else {
+            return
+        }
+
+        let inputs = cbc.inputs.map { input in
+            return delegate.createNode(input.absolutePath)
+        }.filter { node in
+            node.path.fileExtension == "swiftconstvalues"
+        }
+        var outputs = [any PlannedNode]()
+        let outputPath = cbc.output
+        outputs.append(delegate.createNode(outputPath))
+
+
+        let commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString)
+
+        delegate.createTask(type: self,
+                            ruleInfo: defaultRuleInfo(cbc, delegate),
+                            commandLine: commandLine,
+                            environment: environmentFromSpec(cbc, delegate),
+                            workingDirectory: cbc.producer.defaultWorkingDirectory,
+                            inputs: inputs,
+                            outputs: outputs,
+                            action: nil,
+                            execDescription: resolveExecutionDescription(cbc, delegate),
+                            enableSandboxing: enableSandboxing
+        )
+    }
+}
diff --git a/Sources/SWBApplePlatform/ExtensionPointExtractorTaskProducer.swift b/Sources/SWBApplePlatform/ExtensionPointExtractorTaskProducer.swift
new file mode 100644
index 00000000..9a37cbe1
--- /dev/null
+++ b/Sources/SWBApplePlatform/ExtensionPointExtractorTaskProducer.swift
@@ -0,0 +1,171 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import SWBCore
+import SWBUtil
+import SWBMacro
+import SWBTaskConstruction
+
+final class ExtensionPointExtractorTaskProducer: PhasedTaskProducer, TaskProducer {
+
+    override var defaultTaskOrderingOptions: TaskOrderingOptions {
+        return .unsignedProductRequirement
+    }
+
+    private func filterBuildFiles(_ buildFiles: [BuildFile]?, identifiers: [String], buildFilesProcessingContext: BuildFilesProcessingContext) -> [FileToBuild] {
+        guard let buildFiles else {
+            return []
+        }
+
+        let fileTypes = identifiers.compactMap { identifier in
+            context.lookupFileType(identifier: identifier)
+        }
+
+        return fileTypes.flatMap { fileType in
+            buildFiles.compactMap { buildFile in
+                guard let resolvedBuildFileInfo = try? self.context.resolveBuildFileReference(buildFile),
+                      !buildFilesProcessingContext.isExcluded(resolvedBuildFileInfo.absolutePath, filters: buildFile.platformFilters),
+                      resolvedBuildFileInfo.fileType.conformsTo(fileType) else {
+                    return nil
+                }
+
+                return FileToBuild(absolutePath: resolvedBuildFileInfo.absolutePath, fileType: fileType)
+            }
+        }
+    }
+
+    func generateTasks() async -> [any PlannedTask] {
+
+        guard ExtensionPointExtractorSpec.shouldConstructTask(scope: context.settings.globalScope, productType: context.productType, isApplePlatform: context.isApplePlatform) else {
+            return []
+        }
+
+        context.addDeferredProducer {
+
+            let scope = self.context.settings.globalScope
+            let buildFilesProcessingContext = BuildFilesProcessingContext(scope)
+
+            let perArchConstMetadataFiles = self.context.generatedSwiftConstMetadataFiles()
+
+            let constMetadataFiles: [Path]
+            if let firstArch = perArchConstMetadataFiles.keys.sorted().first {
+                constMetadataFiles = perArchConstMetadataFiles[firstArch]!
+            } else {
+                constMetadataFiles = []
+            }
+
+            let constMetadataFilesToBuild = constMetadataFiles.map { absolutePath -> FileToBuild in
+                let fileType = self.context.workspaceContext.core.specRegistry.getSpec("file") as! FileTypeSpec
+                return FileToBuild(absolutePath: absolutePath, fileType: fileType)
+            }
+
+            let inputs = constMetadataFilesToBuild
+            guard inputs.isEmpty == false else {
+                return []
+            }
+
+            var deferredTasks: [any PlannedTask] = []
+
+            let cbc = CommandBuildContext(producer: self.context, scope: scope, inputs: inputs, resourcesDir: buildFilesProcessingContext.resourcesDir)
+            await self.appendGeneratedTasks(&deferredTasks) { delegate in
+                let domain = self.context.settings.platform?.name ?? ""
+                guard let spec = self.context.specRegistry.getSpec("com.apple.compilers.extract-appextensionpoints", domain:domain) as? ExtensionPointExtractorSpec else {
+                    return
+                }
+                await spec.constructTasks(cbc, delegate)
+            }
+
+            return deferredTasks
+        }
+        return []
+    }
+}
+
+
+final class AppExtensionInfoPlistGeneratorTaskProducer: PhasedTaskProducer, TaskProducer {
+
+    override var defaultTaskOrderingOptions: TaskOrderingOptions {
+        return .unsignedProductRequirement
+    }
+
+    private func filterBuildFiles(_ buildFiles: [BuildFile]?, identifiers: [String], buildFilesProcessingContext: BuildFilesProcessingContext) -> [FileToBuild] {
+        guard let buildFiles else {
+            return []
+        }
+
+        let fileTypes = identifiers.compactMap { identifier in
+            context.lookupFileType(identifier: identifier)
+        }
+
+        return fileTypes.flatMap { fileType in
+            buildFiles.compactMap { buildFile in
+                guard let resolvedBuildFileInfo = try? self.context.resolveBuildFileReference(buildFile),
+                      !buildFilesProcessingContext.isExcluded(resolvedBuildFileInfo.absolutePath, filters: buildFile.platformFilters),
+                      resolvedBuildFileInfo.fileType.conformsTo(fileType) else {
+                    return nil
+                }
+
+                return FileToBuild(absolutePath: resolvedBuildFileInfo.absolutePath, fileType: fileType)
+            }
+        }
+    }
+
+    func generateTasks() async -> [any PlannedTask] {
+
+        let scope = context.settings.globalScope
+        let productType = context.productType
+        let isApplePlatform = context.isApplePlatform
+        guard AppExtensionPlistGeneratorSpec.shouldConstructTask(scope: scope, productType: productType, isApplePlatform: isApplePlatform) else {
+            return []
+        }        
+
+        let tasks: [any PlannedTask] = []
+        let buildFilesProcessingContext = BuildFilesProcessingContext(scope)
+
+        let moduelName = context.settings.globalScope.evaluate(BuiltinMacros.TARGET_NAME)
+        let plistPath = buildFilesProcessingContext.tmpResourcesDir.join(Path("\(moduelName)-appextension-generated-info.plist"))
+
+        context.addDeferredProducer {
+
+            let perArchConstMetadataFiles = self.context.generatedSwiftConstMetadataFiles()
+
+            let constMetadataFiles: [Path]
+            if let firstArch = perArchConstMetadataFiles.keys.sorted().first {
+                constMetadataFiles = perArchConstMetadataFiles[firstArch]!
+            } else {
+                constMetadataFiles = []
+            }
+
+            let constMetadataFilesToBuild = constMetadataFiles.map { absolutePath -> FileToBuild in
+                let fileType = self.context.workspaceContext.core.specRegistry.getSpec("file") as! FileTypeSpec
+                return FileToBuild(absolutePath: absolutePath, fileType: fileType)
+            }
+
+            let inputs = constMetadataFilesToBuild
+            var deferredTasks: [any PlannedTask] = []
+
+            let cbc = CommandBuildContext(producer: self.context, scope: scope, inputs: inputs, output: plistPath)
+
+            await self.appendGeneratedTasks(&deferredTasks) { delegate in
+                let domain = self.context.settings.platform?.name ?? ""
+                guard let spec = self.context.specRegistry.getSpec("com.apple.compilers.appextension-plist-generator",domain: domain) as? AppExtensionPlistGeneratorSpec else {
+                    return
+                }
+                await spec.constructTasks(cbc, delegate)
+            }
+
+            return deferredTasks
+        }
+        self.context.addGeneratedInfoPlistContent(plistPath)
+        return tasks
+    }
+}
diff --git a/Sources/SWBApplePlatform/Plugin.swift b/Sources/SWBApplePlatform/Plugin.swift
index 111b9578..adca6f0e 100644
--- a/Sources/SWBApplePlatform/Plugin.swift
+++ b/Sources/SWBApplePlatform/Plugin.swift
@@ -47,9 +47,9 @@ struct TaskProducersExtension: TaskProducerExtension {
     }
 
     var unorderedPostSetupTaskProducers: [any TaskProducerFactory] {
-        [
-            StubBinaryTaskProducerFactory()
-        ]
+        [StubBinaryTaskProducerFactory(),
+         AppExtensionInfoPlistGeneratorTaskProducerFactory(),
+         ExtensionPointExtractorTaskProducerFactory()]
     }
 
     var unorderedPostBuildPhasesTaskProducers: [any TaskProducerFactory] {
@@ -63,6 +63,26 @@ struct TaskProducersExtension: TaskProducerExtension {
     }
 }
 
+struct ExtensionPointExtractorTaskProducerFactory: TaskProducerFactory {
+    var name: String {
+        "ExtensionPointExtractorTaskProducer"
+    }
+
+    func createTaskProducer(_ context: TargetTaskProducerContext, startPhaseNodes: [PlannedVirtualNode], endPhaseNode: PlannedVirtualNode) -> any TaskProducer {
+        ExtensionPointExtractorTaskProducer(context, phaseStartNodes: startPhaseNodes, phaseEndNode: endPhaseNode)
+    }
+}
+
+struct AppExtensionInfoPlistGeneratorTaskProducerFactory: TaskProducerFactory {
+    var name: String {
+        "AppExtensionInfoPlistGeneratorTaskProducer"
+    }
+
+    func createTaskProducer(_ context: TargetTaskProducerContext, startPhaseNodes: [PlannedVirtualNode], endPhaseNode: PlannedVirtualNode) -> any TaskProducer {
+        AppExtensionInfoPlistGeneratorTaskProducer(context, phaseStartNodes: startPhaseNodes, phaseEndNode: endPhaseNode)
+    }
+}
+
 struct StubBinaryTaskProducerFactory: TaskProducerFactory, GlobalTaskProducerFactory {
     var name: String {
         "StubBinaryTaskProducer"
@@ -100,9 +120,11 @@ struct RealityAssetsTaskProducerFactory: TaskProducerFactory {
 struct ApplePlatformSpecsExtension: SpecificationsExtension {
     func specificationClasses() -> [any SpecIdentifierType.Type] {
         [
-            ActoolCompilerSpec.self,
+            AppExtensionPlistGeneratorSpec.self,
             AppIntentsMetadataCompilerSpec.self,
             AppIntentsSSUTrainingCompilerSpec.self,
+            ExtensionPointExtractorSpec.self,
+            ActoolCompilerSpec.self,
             CoreDataModelCompilerSpec.self,
             CoreMLCompilerSpec.self,
             CopyTiffFileSpec.self,
@@ -232,7 +254,12 @@ struct AppleSettingsBuilderExtension: SettingsBuilderExtension {
         ]
     }
 
-    func addBuiltinDefaults(fromEnvironment environment: [String : String], parameters: BuildParameters) throws -> [String : String] { [:] }
+    func addBuiltinDefaults(fromEnvironment environment: [String : String], parameters: BuildParameters) throws -> [String : String] {
+        let appIntentsProtocols = "AppIntent EntityQuery AppEntity TransientEntity AppEnum AppShortcutProviding AppShortcutsProvider AnyResolverProviding AppIntentsPackage DynamicOptionsProvider _IntentValueRepresentable _AssistantIntentsProvider _GenerativeFunctionExtractable IntentValueQuery Resolver"
+        let extensionKitProtocols = "AppExtension ExtensionPointDefining"
+        let constValueProtocols = [appIntentsProtocols, extensionKitProtocols].joined(separator: " ")
+        return ["SWIFT_EMIT_CONST_VALUE_PROTOCOLS" : constValueProtocols]
+    }
     func addOverrides(fromEnvironment: [String : String], parameters: BuildParameters) throws -> [String : String] { [:] }
     func addProductTypeDefaults(productType: ProductTypeSpec) -> [String : String] { [:] }
     func addSDKOverridingSettings(_ sdk: SDK, _ variant: SDKVariant?, _ sparseSDKs: [SDK], specLookupContext: any SWBCore.SpecLookupContext) throws -> [String : String] { [:] }
diff --git a/Sources/SWBApplePlatform/Specs/EXUtil.xcspec b/Sources/SWBApplePlatform/Specs/EXUtil.xcspec
new file mode 100644
index 00000000..5c7cdd4b
--- /dev/null
+++ b/Sources/SWBApplePlatform/Specs/EXUtil.xcspec
@@ -0,0 +1,42 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2024 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+(
+    {
+        Type = Compiler;
+        Identifier = "com.apple.compilers.extract-appextensionpoints";
+        Name = "ExtensionPoint Extractor";
+        Description = "Extracts Extension Point definition";
+        CommandLine = "exutil extract-extension-points --module-name $(PRODUCT_MODULE_NAME) --sdk-root $(SDKROOT) --xcode-version $(XCODE_PRODUCT_BUILD_VERSION) --platform-family $(PLATFORM_FAMILY_NAME) --deployment-target $($(DEPLOYMENT_TARGET_SETTING_NAME)) --bundle-identifier $(PRODUCT_BUNDLE_IDENTIFIER) --output $(TARGET_BUILD_DIR)/$(EXTENSIONS_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).appexpt [inputs]";
+        RuleName = "ExtensionPointExtractor";
+        ExecDescription = "Extract Extension Point definition";
+        ProgressDescription = "Extracting Extension Point definition";
+        Outputs = (
+            "$(TARGET_BUILD_DIR)/$(EXTENSIONS_FOLDER_PATH)/$(PRODUCT_MODULE_NAME).appexpt"
+        );
+        IsArchitectureNeutral = YES;
+        CommandOutputParser = XCGenericCommandOutputParser;
+    },
+
+    {
+        Type = Compiler;
+        Identifier = "com.apple.compilers.appextension-plist-generator";
+        Name = "AppExtension plist generator";
+        Description = "Generates the required ExtensionKit plist keys";
+        CommandLine = "exutil generate-appextension-plist --module-name $(PRODUCT_MODULE_NAME) --sdk-root $(SDKROOT) --xcode-version $(XCODE_PRODUCT_BUILD_VERSION) --platform-family $(PLATFORM_FAMILY_NAME) --deployment-target $($(DEPLOYMENT_TARGET_SETTING_NAME)) --bundle-identifier $(PRODUCT_BUNDLE_IDENTIFIER) --output [output] [inputs]";
+        RuleName = "AppExtensionPListGenerator";
+        ExecDescription = "Generate AppExtension plist";
+        ProgressDescription = "Generating AppExtension plist";
+        IsArchitectureNeutral = YES;
+        CommandOutputParser = XCGenericCommandOutputParser;
+    },
+)
diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift
index 34b1d5ee..04bd1c66 100644
--- a/Sources/SWBCore/Settings/BuiltinMacros.swift
+++ b/Sources/SWBCore/Settings/BuiltinMacros.swift
@@ -663,6 +663,8 @@ public final class BuiltinMacros {
     public static let DISABLE_TASK_SANDBOXING = BuiltinMacros.declareBooleanMacro("DISABLE_TASK_SANDBOXING")
     public static let ENABLE_USER_SCRIPT_SANDBOXING = BuiltinMacros.declareBooleanMacro("ENABLE_USER_SCRIPT_SANDBOXING")
     public static let ENABLE_XOJIT_PREVIEWS = BuiltinMacros.declareBooleanMacro("ENABLE_XOJIT_PREVIEWS")
+    public static let EX_ENABLE_EXTENSION_POINT_GENERATION = BuiltinMacros.declareBooleanMacro("EX_ENABLE_EXTENSION_POINT_GENERATION")
+    public static let EX_DISABLE_APPEXTENSION_ATTRIBUTES_GENERATION = BuiltinMacros.declareBooleanMacro("EX_DISABLE_APPEXTENSION_ATTRIBUTES_GENERATION")
     public static let EXCLUDED_EXPLICIT_TARGET_DEPENDENCIES = BuiltinMacros.declareStringListMacro("EXCLUDED_EXPLICIT_TARGET_DEPENDENCIES")
     public static let EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES = BuiltinMacros.declareStringListMacro("EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES")
     public static let EXCLUDED_SOURCE_FILE_NAMES = BuiltinMacros.declareStringListMacro("EXCLUDED_SOURCE_FILE_NAMES")
@@ -1705,6 +1707,8 @@ public final class BuiltinMacros {
         ENTITLEMENTS_DONT_REMOVE_GET_TASK_ALLOW,
         ENTITLEMENTS_DESTINATION,
         ENTITLEMENTS_REQUIRED,
+        EX_ENABLE_EXTENSION_POINT_GENERATION,
+        EX_DISABLE_APPEXTENSION_ATTRIBUTES_GENERATION,
         EXCLUDED_EXPLICIT_TARGET_DEPENDENCIES,
         EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES,
         EXCLUDED_SOURCE_FILE_NAMES,
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
index f80e2178..0707113e 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
@@ -76,7 +76,7 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
     /// The resources directory for the product, if relevant.
     public let resourcesDir: Path
     /// The resources intermediates directory, if appropriate.
-    let tmpResourcesDir: Path
+    public let tmpResourcesDir: Path
 
     /// True if the build files belongs to the preferred arch among the archs we're processing.
     ///
diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
index 38501ae7..144a07b5 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
@@ -481,7 +481,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
     }
 
     /// Add an info plist addition generated by this target.
-    func addGeneratedInfoPlistContent(_ path: Path) {
+    public func addGeneratedInfoPlistContent(_ path: Path) {
         state.withLock { state in
             assert(!state._inDeferredMode)
             state._generatedInfoPlistContents.append(path)
diff --git a/Sources/SWBUniversalPlatform/Specs/Swift.xcspec b/Sources/SWBUniversalPlatform/Specs/Swift.xcspec
index cccdc078..a3fb17ae 100644
--- a/Sources/SWBUniversalPlatform/Specs/Swift.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/Swift.xcspec
@@ -936,7 +936,6 @@
             {
                 Name = "SWIFT_EMIT_CONST_VALUE_PROTOCOLS";
                 Type = StringList;
-                DefaultValue = "AppIntent EntityQuery AppEntity TransientEntity AppEnum AppShortcutProviding AppShortcutsProvider AnyResolverProviding AppIntentsPackage DynamicOptionsProvider _IntentValueRepresentable _AssistantIntentsProvider _GenerativeFunctionExtractable";
                 DisplayName = "Const value emission protocol list";
                 Description = "A list of protocol names whose conformances the Swift compiler is to emit compile-time-known values for.";
             },
diff --git a/Tests/SWBApplePlatformTests/EXUtilTaskConstructionTests.swift b/Tests/SWBApplePlatformTests/EXUtilTaskConstructionTests.swift
new file mode 100644
index 00000000..efda1c9e
--- /dev/null
+++ b/Tests/SWBApplePlatformTests/EXUtilTaskConstructionTests.swift
@@ -0,0 +1,155 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+import Testing
+import SWBCore
+import SWBProtocol
+import SWBTaskConstruction
+import SWBTestSupport
+import SWBUtil
+
+
+@Suite
+fileprivate struct EXUtilTaskConstructionTests: CoreBasedTests {
+
+    @Test(.requireSDKs(.iOS), .requireMinimumSDKBuildVersion(sdkName: KnownSDK.iOS.sdkName, requiredVersion: "23A213"))
+    func extractExtensionPoint() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = try await TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("source.swift"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "AD_HOC_CODE_SIGNING_ALLOWED": "YES",
+                            "ARCHS": "arm64",
+                            "CODE_SIGN_IDENTITY": "-",
+                            "GENERATE_INFOPLIST_FILE": "YES",
+                            "PRODUCT_BUNDLE_IDENTIFIER": "com.foo.bar",
+                            "PRODUCT_NAME": "$(TARGET_NAME)",
+                            "SDKROOT": "iphoneos",
+                            "SWIFT_EXEC": swiftCompilerPath.str,
+                            "SWIFT_VERSION": swiftVersion,
+                            "VERSIONING_SYSTEM": "apple-generic",
+                            "SWIFT_EMIT_CONST_VALUE_PROTOCOLS": "Foo Bar",
+                        ]),
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "ExtensionPointTest",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "LM_ENABLE_LINK_GENERATION": "YES",
+                                    "EX_ENABLE_EXTENSION_POINT_GENERATION" : "YES"
+                                ]),
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase(["source.swift"]),
+                        ]
+                    )
+                ])
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            await tester.checkBuild(runDestination: .iOS) { results in
+
+                results.checkTask(.matchRuleType("ExtensionPointExtractor")) { task in
+                    task.checkCommandLineMatches(["exutil", "extract-extension-points", .anySequence])
+                    task.checkInputs(contain: [.name("source.swiftconstvalues")])
+                    task.checkOutputs(contain: [.namePattern(.suffix("-generated.appexpt"))])
+                    results.checkNoDiagnostics()
+
+                }
+                results.checkTask(.matchRuleType("SwiftDriver Compilation")) { task in
+                    task.checkCommandLineContains(["-emit-const-values"])
+                }
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.iOS), .requireMinimumSDKBuildVersion(sdkName: KnownSDK.iOS.sdkName, requiredVersion: "23A213"))
+    func generateExtensionPlist() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = try await TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("source.swift"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "AD_HOC_CODE_SIGNING_ALLOWED": "YES",
+                            "ARCHS": "arm64",
+                            "CODE_SIGN_IDENTITY": "-",
+                            "GENERATE_INFOPLIST_FILE": "YES",
+                            "PRODUCT_BUNDLE_IDENTIFIER": "com.foo.bar",
+                            "PRODUCT_NAME": "$(TARGET_NAME)",
+                            "SDKROOT": "iphoneos",
+                            "SWIFT_EXEC": swiftCompilerPath.str,
+                            "SWIFT_VERSION": swiftVersion,
+                            "VERSIONING_SYSTEM": "apple-generic",
+                            "SWIFT_EMIT_CONST_VALUE_PROTOCOLS": "Foo Bar",
+                        ]),
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppExtensionPlistGeneratorTest",
+                        type: .extensionKitExtension,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [:]),
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase(["source.swift"]),
+                        ]
+                    )
+                ])
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            try await tester.checkBuild(runDestination: .iOS) { results in
+
+                let generatorTask = try #require(results.checkTask(.matchRuleType("AppExtensionPListGenerator")) { task in
+                    task.checkCommandLineMatches(["exutil", "generate-appextension-plist", .anySequence])
+                    task.checkInputs(contain: [.name("source.swiftconstvalues")])
+                    task.checkOutputs(contain: [.namePattern(.suffix("appextension-generated-info.plist"))])
+                    results.checkNoDiagnostics()
+                    return task
+                })
+
+                results.checkTask(.matchRuleType("ProcessInfoPlistFile")) { task in
+                    results.checkTaskFollows(task, antecedent: generatorTask)
+                }
+
+                results.checkTask(.matchRuleType("SwiftDriver Compilation")) { task in
+                    task.checkCommandLineContains(["-emit-const-values"])
+                }
+            }
+        }
+    }
+
+}
diff --git a/Tests/SWBTaskConstructionTests/AppExtensionTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/AppExtensionTaskConstructionTests.swift
index 5832096a..f84215a3 100644
--- a/Tests/SWBTaskConstructionTests/AppExtensionTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/AppExtensionTaskConstructionTests.swift
@@ -69,6 +69,99 @@ fileprivate struct AppExtensionTaskConstructionTests: CoreBasedTests {
                     ],
                     buildPhases: [
                         TestSourcesBuildPhase(["Source.c"]),
+                        TestCopyFilesBuildPhase([
+                            TestBuildFile(.target("Legacy"))], destinationSubfolder: .plugins, onlyForDeployment: false),
+                        TestCopyFilesBuildPhase([
+                            TestBuildFile(.target("Modern"))], destinationSubfolder: .builtProductsDir, destinationSubpath: "$(EXTENSIONS_FOLDER_PATH)", onlyForDeployment: false),
+                    ],
+                    dependencies: ["Legacy", "Modern"]),
+                TestStandardTarget(
+                    "Legacy",
+                    type: .applicationExtension,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:])
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["Source.c"]),
+                    ]),
+                TestStandardTarget(
+                    "Modern",
+                    type: .extensionKitExtension,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:])
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["Source.c"]),
+                    ]),
+            ])
+        let tester = try await TaskConstructionTester(getCore(), testProject)
+
+        await tester.checkBuild(BuildParameters(action: .build, configuration: "Debug"), runDestination: .macOS) { results in
+            results.checkNoDiagnostics()
+            results.checkTask(.matchTargetName("Foo"), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug/Foo.app/Contents/Extensions/Modern.appex", "/tmp/Test/aProject/build/Debug/Modern.appex"])) { task in }
+            results.checkTask(.matchTargetName("Foo"), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug/Foo.app/Contents/PlugIns/Legacy.appex", "/tmp/Test/aProject/build/Debug/Legacy.appex"])) { task in }
+        }
+
+        await tester.checkBuild(BuildParameters(action: .build, configuration: "Debug"), runDestination: .iOS) { results in
+            results.checkNoDiagnostics()
+            results.checkTask(.matchTargetName("Foo"), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug-iphoneos/Foo.app/Extensions/Modern.appex", "/tmp/Test/aProject/build/Debug-iphoneos/Modern.appex"])) { task in }
+            results.checkTask(.matchTargetName("Foo"), .matchRule(["Copy", "/tmp/Test/aProject/build/Debug-iphoneos/Foo.app/PlugIns/Legacy.appex", "/tmp/Test/aProject/build/Debug-iphoneos/Legacy.appex"])) { task in }
+        }
+    }
+
+    @Test(.requireSDKs(.macOS, .iOS))
+    func appExtensionSwiftEmbedding() async throws {
+        let testProject = try await TestProject(
+            "aProject",
+            groupTree: TestGroup(
+                "Sources", children: [
+                    TestFile("source.swift"),
+                    TestFile("Legacy.appex", fileType: "wrapper.app-extension", sourceTree: .buildSetting("BUILT_PRODUCTS_DIR")),
+                    TestFile("Modern.appex", fileType: "wrapper.extensionkit-extension", sourceTree: .buildSetting("BUILT_PRODUCTS_DIR")),
+                ]),
+            buildConfigurations: [
+                TestBuildConfiguration("Debug", buildSettings: [
+                    "CODE_SIGNING_ALLOWED": "NO",
+                    "GENERATE_INFOPLIST_FILE": "YES",
+                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                    "SUPPORTED_PLATFORMS": "macosx iphoneos",
+                    "SWIFT_EXEC": swiftCompilerPath.str,
+                    "SWIFT_VERSION": swiftVersion,
+                    "SWIFT_EMIT_CONST_VALUE_PROTOCOLS": "Foo Bar",
+                ])
+            ],
+            targets: [
+                TestStandardTarget(
+                    "Foo",
+                    type: .application,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:])
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["source.swift"]),
+                        TestCopyFilesBuildPhase([
+                            TestBuildFile(.target("Legacy"))], destinationSubfolder: .plugins, onlyForDeployment: false),
+                        TestCopyFilesBuildPhase([
+                            TestBuildFile(.target("Modern"))], destinationSubfolder: .builtProductsDir, destinationSubpath: "$(EXTENSIONS_FOLDER_PATH)", onlyForDeployment: false),
+                    ],
+                    dependencies: ["Legacy", "Modern"]),
+                TestStandardTarget(
+                    "Legacy",
+                    type: .applicationExtension,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:])
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["source.swift"]),
+                    ]),
+                TestStandardTarget(
+                    "Modern",
+                    type: .extensionKitExtension,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:])
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["source.swift"]),
                     ]),
             ])
         let tester = try await TaskConstructionTester(getCore(), testProject)

From 558048b7c1664be905fef4065128dba65cfe3dde Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 11 Jul 2025 14:53:27 -0700
Subject: [PATCH 04/49] Fix compilation error when
 USE_STATIC_PLUGIN_INITIALIZATION is off

---
 Sources/SWBCore/Core.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/SWBCore/Core.swift b/Sources/SWBCore/Core.swift
index 238a2e92..7686414d 100644
--- a/Sources/SWBCore/Core.swift
+++ b/Sources/SWBCore/Core.swift
@@ -330,7 +330,7 @@ public final class Core: Sendable {
         // Search paths relative to SWBCore itself.
         do {
             func appendInferior(_ path: Path) {
-                if !developerPath.dirname.isAncestor(of: path) {
+                if !developerPath.path.dirname.isAncestor(of: path) {
                     result.append(path)
                 }
             }

From 58ce7ebfb38e8a1fae331b29ef2011e966e9594b Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Mon, 14 Jul 2025 10:52:25 -0700
Subject: [PATCH 05/49] Pass -platform_version to the linker when prelinking a
 target on Darwin platforms

rdar://154346861
---
 .../Tools/PrelinkedObjectLink.swift           |  7 ++
 Tests/SWBBuildSystemTests/LinkerTests.swift   | 68 +++++++++++++++++++
 .../PrelinkedObjectFileTests.swift            | 10 +--
 .../TaskConstructionTests.swift               |  2 +-
 4 files changed, 81 insertions(+), 6 deletions(-)

diff --git a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift
index d651edfb..d2e17180 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/PrelinkedObjectLink.swift
@@ -35,6 +35,13 @@ 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]
+        }
+
         // 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 <rdar://problem/51800525> for more about the history here.
 
         let sysroot = cbc.scope.evaluate(BuiltinMacros.SDK_DIR)
diff --git a/Tests/SWBBuildSystemTests/LinkerTests.swift b/Tests/SWBBuildSystemTests/LinkerTests.swift
index 36833664..97f61702 100644
--- a/Tests/SWBBuildSystemTests/LinkerTests.swift
+++ b/Tests/SWBBuildSystemTests/LinkerTests.swift
@@ -360,4 +360,72 @@ fileprivate struct LinkerTests: CoreBasedTests {
             }
         }
     }
+
+    @Test(.requireSDKs(.macOS))
+    func prelinkingPropagatesPlatformVersion() async throws {
+        func createProject(_ tmpDir: Path, enableInterop: Bool) -> TestProject {
+            TestProject(
+                "TestProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("main.c"),
+                        TestFile("lib.c"),
+                    ]),
+                targets: [
+                    TestStandardTarget(
+                        "cli", type: .commandLineTool,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "CODE_SIGNING_ALLOWED": "NO",
+                                    "GENERATE_PRELINK_OBJECT_FILE": "YES",
+                                    "GCC_SYMBOLS_PRIVATE_EXTERN": "NO",
+                                    "MACOSX_DEPLOYMENT_TARGET": "13.0",
+                                ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase(["main.c"]),
+                            TestFrameworksBuildPhase([TestBuildFile(.target("lib"))])
+                        ],
+                        dependencies: [TestTargetDependency("lib")]
+                    ),
+                    TestStandardTarget(
+                        "lib", type: .staticLibrary,
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "MACOSX_DEPLOYMENT_TARGET": "13.0",
+                                ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase(["lib.c"])
+                        ]
+                    ),
+                ])
+        }
+
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = createProject(tmpDir, enableInterop: true)
+            let tester = try await BuildOperationTester(getCore(), testProject, simulated: false)
+            let projectDir = tester.workspace.projects[0].sourceRoot
+            try await tester.fs.writeFileContents(projectDir.join("main.c")) { stream in
+                stream <<< "int foo(void); int main(void) { foo(); }"
+            }
+            try await tester.fs.writeFileContents(projectDir.join("lib.c")) { stream in
+                stream <<< "int foo(void) { return 42; }"
+            }
+            try await tester.checkBuild(runDestination: .macOS) { results in
+                results.checkNoDiagnostics()
+                results.checkTask(.matchRuleType("PrelinkedObjectLink")) { task in
+                    task.checkCommandLineContainsUninterrupted(["-platform_version", "1", "13.0"])
+                }
+            }
+        }
+    }
 }
diff --git a/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift b/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift
index 3c805622..45ee228d 100644
--- a/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift
+++ b/Tests/SWBTaskConstructionTests/PrelinkedObjectFileTests.swift
@@ -73,7 +73,7 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests {
             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", "x86_64", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-exported_symbols_list", "Exports.exp", "-lWarningLibrary", "-lSomeLibrary", "-lAnotherLibrary", "-o", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
+                    task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", "x86_64", "-platform_version", "1", .any, .any, "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-exported_symbols_list", "Exports.exp", "-lWarningLibrary", "-lSomeLibrary", "-lAnotherLibrary", "-o", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
                 }
                 results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in
                     task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", "x86_64", "-D", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("-L\(SRCROOT)/build/Debug"), "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/x86_64/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/x86_64/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug/libAllLibraries.a")])
@@ -102,7 +102,7 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests {
             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", "x86_64", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-exported_symbols_list", "Exports.exp", "-lWarningLibrary", "-lSomeLibrary", "-lAnotherLibrary", "-o", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
+                    task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", "x86_64", "-platform_version", "1", .any, .any, "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-exported_symbols_list", "Exports.exp", "-lWarningLibrary", "-lSomeLibrary", "-lAnotherLibrary", "-o", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
                 }
                 results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in
                     task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", "x86_64", "-D", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("-L\(SRCROOT)/build/Debug/BuiltProducts"), "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/x86_64/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug/AllLibraries.build/Objects-normal/x86_64/AllLibraries_libtool_dependency_info.dat", "-o", "/tmp/aProject.dst/usr/local/lib/libAllLibraries.a"])
@@ -203,7 +203,7 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests {
             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", "x86_64", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-maccatalyst/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
+                    task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", "x86_64", "-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-x86_64-prelink.o")])
                 }
                 results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in
                     task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", "x86_64", "-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/x86_64/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug-maccatalyst/AllLibraries.build/Objects-normal/x86_64/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug-maccatalyst/libAllLibraries.a")])
@@ -272,7 +272,7 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests {
             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", "arm64", "-syslibroot", .equal(results.runDestinationSDK.path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-iphoneos/AllLibraries.build/Objects-normal/libAllLibraries.a-arm64-prelink.o")])
+                    task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", "arm64", "-platform_version", "2", .any, .any, "-syslibroot", .equal(results.runDestinationSDK.path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-iphoneos/AllLibraries.build/Objects-normal/libAllLibraries.a-arm64-prelink.o")])
                 }
                 results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in
                     task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", "arm64", "-D", "-syslibroot", .equal(results.runDestinationSDK.path.str), .equal("-L\(SRCROOT)/build/Debug-iphoneos"), "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug-iphoneos/AllLibraries.build/Objects-normal/arm64/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug-iphoneos/AllLibraries.build/Objects-normal/arm64/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug-iphoneos/libAllLibraries.a")])
@@ -340,7 +340,7 @@ fileprivate struct PrelinkedObjectFileTests: CoreBasedTests {
             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", "x86_64", "-syslibroot", .equal(results.runDestinationSDK.path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-iphonesimulator/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
+                    task.checkCommandLineMatches([.suffix("ld"), "-r", "-arch", "x86_64", "-platform_version", "7", .any, .any, "-syslibroot", .equal(results.runDestinationSDK.path.str), "-o", .equal("\(SRCROOT)/build/aProject.build/Debug-iphonesimulator/AllLibraries.build/Objects-normal/libAllLibraries.a-x86_64-prelink.o")])
                 }
                 results.checkTask(.matchTarget(target), .matchRuleType("Libtool")) { task in
                     task.checkCommandLineMatches([.suffix("libtool"), "-static", "-arch_only", "x86_64", "-D", "-syslibroot", .equal(results.runDestinationSDK.path.str), .equal("-L\(SRCROOT)/build/Debug-iphonesimulator"), "-filelist", .equal("\(SRCROOT)/build/aProject.build/Debug-iphonesimulator/AllLibraries.build/Objects-normal/x86_64/AllLibraries.LinkFileList"), "-dependency_info", "\(SRCROOT)/build/aProject.build/Debug-iphonesimulator/AllLibraries.build/Objects-normal/x86_64/AllLibraries_libtool_dependency_info.dat", "-o", .equal("\(SRCROOT)/build/Debug-iphonesimulator/libAllLibraries.a")])
diff --git a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
index a3931342..d2c11ff1 100644
--- a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
@@ -895,7 +895,7 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                         }
 
                         #expect(nonUniqueObjs[0] != nonUniqueObjs[1])
-                        task.checkCommandLineMatches([StringPattern.suffix("ld"), "-r", "-arch", "x86_64", "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile-Matched-Excluded.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile-Matched-Included.o"), .equal(nonUniqueObjs[0]), .equal(nonUniqueObjs[1]), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile_MRR.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Lex.yy.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/y.tab.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Script-Output-Custom-SourceFile.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Script-Output-Standard-SourceFile.o"), "-o", .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/AppTarget-x86_64-prelink.o")])
+                        task.checkCommandLineMatches([StringPattern.suffix("ld"), "-r", "-arch", "x86_64", "-platform_version", "1", .any, .any, "-syslibroot", .equal(core.loadSDK(.macOS).path.str), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile-Matched-Excluded.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile-Matched-Included.o"), .equal(nonUniqueObjs[0]), .equal(nonUniqueObjs[1]), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile_MRR.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Lex.yy.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/y.tab.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Script-Output-Custom-SourceFile.o"), .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/Script-Output-Standard-SourceFile.o"), "-o", .equal("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/AppTarget-x86_64-prelink.o")])
 
                         task.checkInputs([
                             .path("\(SRCROOT)/build/aProject.build/Release/AppTarget.build/Objects-normal/x86_64/SourceFile.o"),

From 476425488136819bbf7ed2eecafeb585ef2c8142 Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Mon, 14 Jul 2025 18:02:03 -0700
Subject: [PATCH 06/49] Loosen test expectations in framework pruning tests

rdar://155531620
---
 .../BuildOperationTests.swift                 | 54 ++-----------------
 1 file changed, 5 insertions(+), 49 deletions(-)

diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
index 3d5f3dde..68dd3c20 100644
--- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
@@ -2928,12 +2928,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                 "Versions/A/Resources/Info.plist",
                 "Versions/A/Resources/ja.lproj",
                 "Versions/A/Resources/ja.lproj/Localizable.strings",
-                "Versions/A/_CodeSignature",
-                "Versions/A/_CodeSignature/CodeDirectory",
-                "Versions/A/_CodeSignature/CodeRequirements",
-                "Versions/A/_CodeSignature/CodeRequirements-1",
-                "Versions/A/_CodeSignature/CodeResources",
-                "Versions/A/_CodeSignature/CodeSignature",
                 "Versions/Current",
             ]
 
@@ -2970,12 +2964,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     "Versions/A/Headers/AFwk.h",
                     "Versions/A/Resources/AResource.plist",
                     "Versions/A/Resources/Info.plist",
-                    "Versions/A/_CodeSignature",
-                    "Versions/A/_CodeSignature/CodeDirectory",
-                    "Versions/A/_CodeSignature/CodeRequirements",
-                    "Versions/A/_CodeSignature/CodeRequirements-1",
-                    "Versions/A/_CodeSignature/CodeResources",
-                    "Versions/A/_CodeSignature/CodeSignature",
                     "Versions/Current",
                 ])
             }
@@ -3061,7 +3049,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                 ]))
             }
 
-            let sourceDynamicFrameworkFiles = try tester.fs.traverse(sourceDynamicFrameworkPath, { $0.relativeSubpath(from: sourceDynamicFrameworkPath) }).sorted()
+            let sourceDynamicFrameworkFiles = try tester.fs.traverse(sourceDynamicFrameworkPath, { $0.relativeSubpath(from: sourceDynamicFrameworkPath) }).sorted().filter { !$0.contains("_CodeSignature") }
             if runDestination.platform == "macosx" {
                 XCTAssertEqualSequences(sourceDynamicFrameworkFiles, [
                     "ADynamicFwk",
@@ -3075,12 +3063,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     "Versions/A/Resources",
                     "Versions/A/Resources/ADynamicResource.plist",
                     "Versions/A/Resources/Info.plist",
-                    "Versions/A/_CodeSignature",
-                    "Versions/A/_CodeSignature/CodeDirectory",
-                    "Versions/A/_CodeSignature/CodeRequirements",
-                    "Versions/A/_CodeSignature/CodeRequirements-1",
-                    "Versions/A/_CodeSignature/CodeResources",
-                    "Versions/A/_CodeSignature/CodeSignature",
                     "Versions/Current",
                 ])
             } else {
@@ -3090,8 +3072,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     "Headers",
                     "Headers/ADynamicFwk.h",
                     "Info.plist",
-                    "_CodeSignature",
-                    "_CodeSignature/CodeResources",
                 ])
             }
 
@@ -3111,7 +3091,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                 ]))
             }
 
-            let sourceStaticFrameworkFiles = try tester.fs.traverse(sourceStaticFrameworkPath, { $0.relativeSubpath(from: sourceStaticFrameworkPath) }).sorted()
+            let sourceStaticFrameworkFiles = try tester.fs.traverse(sourceStaticFrameworkPath, { $0.relativeSubpath(from: sourceStaticFrameworkPath) }).sorted().filter { !$0.contains("_CodeSignature") }
             if runDestination.platform == "macosx" {
                 XCTAssertEqualSequences(sourceStaticFrameworkFiles, [
                     "AStaticFwk",
@@ -3125,12 +3105,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     "Versions/A/Resources",
                     "Versions/A/Resources/AStaticResource.plist",
                     "Versions/A/Resources/Info.plist",
-                    "Versions/A/_CodeSignature",
-                    "Versions/A/_CodeSignature/CodeDirectory",
-                    "Versions/A/_CodeSignature/CodeRequirements",
-                    "Versions/A/_CodeSignature/CodeRequirements-1",
-                    "Versions/A/_CodeSignature/CodeResources",
-                    "Versions/A/_CodeSignature/CodeSignature",
                     "Versions/Current",
                 ])
             } else {
@@ -3140,12 +3114,6 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     "Headers",
                     "Headers/AStaticFwk.h",
                     "Info.plist",
-                    "_CodeSignature",
-                    "_CodeSignature/CodeDirectory",
-                    "_CodeSignature/CodeRequirements",
-                    "_CodeSignature/CodeRequirements-1",
-                    "_CodeSignature/CodeResources",
-                    "_CodeSignature/CodeSignature",
                 ])
             }
 
@@ -3199,7 +3167,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                             task.checkCommandLine(["builtin-copy", "-exclude", ".DS_Store", "-exclude", "CVS", "-exclude", ".svn", "-exclude", ".git", "-exclude", ".hg", "-exclude", "Headers", "-exclude", "PrivateHeaders", "-exclude", "Modules", "-exclude", "*.tbd", "-resolve-src-symlinks"] + (keepStaticBinary ? [] : ["-remove-static-executable"]) + ["\(tmpDirPath.str)/ADynamicFwk.framework", frameworkDestinationDir])
 
                             let destDynamicFrameworkPath = Path("\(frameworkDestinationDir)/ADynamicFwk.framework")
-                            let delta = try Set(sourceDynamicFrameworkFiles).diff(against: tester.fs.traverse(destDynamicFrameworkPath, { $0.relativeSubpath(from: destDynamicFrameworkPath) }))
+                            let delta = try Set(sourceDynamicFrameworkFiles).diff(against: tester.fs.traverse(destDynamicFrameworkPath, { $0.relativeSubpath(from: destDynamicFrameworkPath) }).filter{ !$0.contains("_CodeSignature") })
                             XCTAssertEqualSequences(delta.right, [])
 
                             // Check that we removed all the headers
@@ -3221,7 +3189,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                             task.checkCommandLine(["builtin-copy", "-exclude", ".DS_Store", "-exclude", "CVS", "-exclude", ".svn", "-exclude", ".git", "-exclude", ".hg", "-exclude", "Headers", "-exclude", "PrivateHeaders", "-exclude", "Modules", "-exclude", "*.tbd", "-resolve-src-symlinks"] + (keepStaticBinary ? [] : ["-remove-static-executable"]) + ["\(tmpDirPath.str)/AStaticFwk.framework", frameworkDestinationDir])
 
                             let destStaticFrameworkPath = Path("\(frameworkDestinationDir)/AStaticFwk.framework")
-                            let delta = try Set(sourceStaticFrameworkFiles).diff(against: tester.fs.traverse(destStaticFrameworkPath, { $0.relativeSubpath(from: destStaticFrameworkPath) }))
+                            let delta = try Set(sourceStaticFrameworkFiles).diff(against: tester.fs.traverse(destStaticFrameworkPath, { $0.relativeSubpath(from: destStaticFrameworkPath) }).filter{ !$0.contains("_CodeSignature") })
                             XCTAssertEqualSequences(delta.right, [])
 
                             // Check that we removed all the headers, as well as the binary (since it is static), if configured
@@ -3240,22 +3208,10 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                                         "Headers/AStaticFwk.h",
                                     ])
                                 } else if useAppStoreCodelessFrameworksWorkaround {
-                                    let additionalFiles: [String]
-                                    if codesign {
-                                        additionalFiles = [
-                                            "_CodeSignature/CodeDirectory",
-                                            "_CodeSignature/CodeRequirements",
-                                            "_CodeSignature/CodeRequirements-1",
-                                            "_CodeSignature/CodeSignature",
-                                        ]
-                                    } else {
-                                        additionalFiles = []
-                                    }
-
                                     XCTAssertEqualSequences(delta.left.sorted(), [
                                         "Headers",
                                         "Headers/AStaticFwk.h",
-                                    ] + additionalFiles)
+                                    ])
                                 } else {
                                     XCTAssertEqualSequences(delta.left.sorted(), [
                                         "AStaticFwk",

From 581f6284c6e158046436f1233f705985b4cd42e8 Mon Sep 17 00:00:00 2001
From: Bob Wilson <bob.wilson@apple.com>
Date: Mon, 21 Jul 2025 17:51:53 -0700
Subject: [PATCH 07/49] Remove duplicate module names in diagnostic about
 missing dependencies

The Clang tracing information currently shows each header file separately,
so if there are multiple headers included from the same module, we get
duplicates. We are investigating having Clang trace the modules directly
and also investigating de-duplicating these diagnostics across targets, but
this is easy to fix in the meantime.

rdar://156160529
---
 Sources/SWBCore/Dependencies.swift                        | 6 +++---
 Tests/SWBBuildSystemTests/DependencyValidationTests.swift | 5 +++--
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index 7fa0328e..0429c616 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -105,17 +105,17 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
 
         let moduleDependencyNames = moduleDependencies.map { $0.name }
         let fileNames = files.compactMap { findFrameworkName($0) }
-        let missingDeps = fileNames.filter {
+        let missingDeps = Set(fileNames.filter {
             return !moduleDependencyNames.contains($0)
         }.map {
             ModuleDependency(name: $0, accessLevel: .Private)
-        }
+        })
 
         guard !missingDeps.isEmpty else { return [] }
 
         let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
 
-        let fixIt = fixItContext?.makeFixIt(newModules: missingDeps)
+        let fixIt = fixItContext?.makeFixIt(newModules: Array(missingDeps))
         let fixIts = fixIt.map { [$0] } ?? []
 
         let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 21483f31..85204c76 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -481,7 +481,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                                     "CLANG_ENABLE_MODULES": "YES",
                                     "CLANG_ENABLE_EXPLICIT_MODULES": "YES",
                                     "GENERATE_INFOPLIST_FILE": "YES",
-                                    "MODULE_DEPENDENCIES": "Foundation",
+                                    "MODULE_DEPENDENCIES": "Accelerate",
                                     "VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR",
                                     "SDKROOT": "$(HOST_PLATFORM)",
                                     "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
@@ -507,6 +507,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
             try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in
                 contents <<< """
                     #include <Foundation/Foundation.h>
+                    #include <Foundation/NSObject.h>
                     #include <Accelerate/Accelerate.h>
 
                     void f0(void) { };
@@ -515,7 +516,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
 
             // Expect complaint about undeclared dependency
             try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
-                results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Accelerate"))
+                results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Foundation (for task"))
             }
 
             // Declaring dependencies resolves the problem

From bb039dab025c49bd723664a8675545f8508d4667 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 11 Jul 2025 10:23:42 -0700
Subject: [PATCH 08/49] Fix hang in ServiceConsoleTests.serviceShutdown

This hang occurred only in CI environments and only on Linux. Here's the sequence of events:

- Test terminates swbuild using SIGKILL
- OS reparents SWBBuildService (a subprocess of swbuild) to launchd (Darwin) / init (others)
- OS closes the file descriptors for the I/O pipes swbuild has connected to SWBBuildService
- SWBBuildService's read() loop indicates EOF due to the broken pipe
- SWBBuildService causes itself to exit

At this point, the getpgid loop should return ERSCH and terminate the test. However, SWBBuildService is sticking around as a zombie for an extended period of time without init reaping the pid, causing getpgid to never hit the termination state. This causes the test to hang indefinitely.

To fix this, there are two aspects:

- A timeout is added around the termination monitoring loop that forces the exit promise to be fulfilled with an error if a 30-second interval elapses without the process exiting
- We switch from using a getpgid loop to using a waitid loop, where the terminal state is that the process has _exited_... we don't care if the zombie hasn't been collected by init, only that it's not in a running state

This fixes the hang for both the Jenkins based CI as well as GitHub actions, and also insulates us against future hangs by ensuring the test will terminate with a timeout error instead of hanging indefinitely, so that we at least know _which_ test is the problem.
---
 .../ConsoleCommands/ServiceConsoleTests.swift | 96 +++++++++++++++----
 1 file changed, 78 insertions(+), 18 deletions(-)

diff --git a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
index fa107c6b..9034ab94 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
@@ -19,7 +19,13 @@ import SWBUtil
 import WinSDK
 #endif
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+#if canImport(System)
+import System
+#else
+import SystemPackage
+#endif
+
+@Suite(.skipHostOS(.windows))
 fileprivate struct ServiceConsoleTests {
     @Test
     func emptyInput() async throws {
@@ -92,7 +98,13 @@ fileprivate struct ServiceConsoleTests {
             await #expect(try cli.exitStatus.wasSignaled)
 
             // Now wait for the service subprocess to exit, without any further communication.
-            try await serviceExitPromise.value
+            try await withTimeout(timeout: .seconds(30), description: "Service process exit promise 30-second limit") {
+                try await withTaskCancellationHandler {
+                    try await serviceExitPromise.value
+                } onCancel: {
+                    serviceExitPromise.fail(throwing: CancellationError())
+                }
+            }
         }
     }
 
@@ -124,6 +136,31 @@ private var SYNCHRONIZE: DWORD {
 }
 
 extension HANDLE: @retroactive @unchecked Sendable {}
+
+func WaitForSingleObjectAsync(_ handle: HANDLE) async throws {
+    var waitHandle: HANDLE?
+    defer {
+        if let waitHandle {
+            _ = UnregisterWait(waitHandle)
+        }
+    }
+
+    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
+        if !RegisterWaitForSingleObject(
+            &waitHandle,
+            handle,
+            { context, _ in
+                let continuation = Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation<Void, any Error>
+                continuation.resume()
+            },
+            Unmanaged.passRetained(continuation as AnyObject).toOpaque(),
+            INFINITE,
+            ULONG(WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)
+        ) {
+            continuation.resume(throwing: Win32Error(GetLastError()))
+        }
+    }
+}
 #endif
 
 extension Processes {
@@ -134,24 +171,31 @@ extension Processes {
             throw Win32Error(GetLastError())
         }
         defer { CloseHandle(proc) }
-        Thread.detachNewThread {
-            if WaitForSingleObject(proc, INFINITE) == WAIT_FAILED {
-                promise.fail(throwing: Win32Error(GetLastError()))
-                return
-            }
-            promise.fulfill(with: ())
+        Task<Void, Never> {
+            await promise.fulfill(with: Result.catching { try await WaitForSingleObjectAsync(proc) })
         }
         #else
-        Task<Void, Never>.detached {
-            while true {
-                // We use getpgid() here to detect when the process has exited (it is not a child). Surprisingly, getpgid() is substantially faster than using kill(pid, 0) here because kill still returns success for zombies, and the service has been reparented to launchd. // ignore-unacceptable-language; POSIX API
-                do {
-                    if getpgid(pid) < 0 {
-                        // We expect the signal to eventually fail with "No such process".
-                        if errno != ESRCH {
-                            throw StubError.error("unexpected exit code: \(errno)")
+        Task<Void, Never> {
+            func wait(pid: pid_t) throws -> Bool {
+                repeat {
+                    do {
+                        var siginfo = siginfo_t()
+                        if waitid(P_PID, id_t(pid), &siginfo, WEXITED | WNOWAIT | WNOHANG) != 0 {
+                            throw Errno(rawValue: errno)
                         }
-                        break
+                        return siginfo.si_pid == pid
+                    } catch Errno.noChildProcess {
+                        return true
+                    } catch Errno.interrupted {
+                        // ignore
+                    }
+                } while true
+            }
+            while !Task.isCancelled {
+                do {
+                    if try wait(pid: pid) {
+                        promise.fulfill(with: ())
+                        return
                     }
                     try await Task.sleep(for: .microseconds(1000))
                 } catch {
@@ -159,9 +203,25 @@ extension Processes {
                     return
                 }
             }
-            promise.fulfill(with: ())
+            promise.fail(throwing: CancellationError())
         }
         #endif
         return promise
     }
 }
+
+#if !os(Windows) && !canImport(Darwin) && !os(FreeBSD)
+fileprivate extension siginfo_t {
+    var si_pid: pid_t {
+        #if os(OpenBSD)
+        return _data._proc._pid
+        #elseif canImport(Glibc)
+        return _sifields._sigchld.si_pid
+        #elseif canImport(Musl)
+        return __si_fields.__si_common.__first.__piduid.si_pid
+        #elseif canImport(Bionic)
+        return _sifields._kill._pid
+        #endif
+    }
+}
+#endif

From 20631694eacc4b9223c18e1cbc866ea926fb3afe Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 11 Jul 2025 10:23:42 -0700
Subject: [PATCH 09/49] Tighten up some logic around file permissions

Fix a regression in PbxCp which caused files to inappropriately gain the execute bit. `isExecutable` is conceptually a higher level operation which (per POSIX) can return true even if the file has no execute permission bits set:

"If execute permission is requested, access shall be granted if execute permission is granted to at least one user by the file permission bits or by an alternate access control mechanism"

No test because it's not clear what the concrete conditions are under which this occurs. That said, copying now checks against the owner permission bit specifically, as that's the actual intent of what the code is meaning to do and matches the original behavior.

Additionally, fix some missing permission information in getFileInfo for the PseudoFS implementation and increase test coverage a bit. Avoids it coming back to bite later.

rdar://154663442
---
 Sources/SWBUtil/FSProxy.swift         | 56 ++++++++++++++-------------
 Sources/SWBUtil/PbxCp.swift           |  3 +-
 Tests/SWBUtilTests/FSProxyTests.swift | 24 +++++++++++-
 3 files changed, 54 insertions(+), 29 deletions(-)

diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift
index f3d53e2c..ddd3c5a9 100644
--- a/Sources/SWBUtil/FSProxy.swift
+++ b/Sources/SWBUtil/FSProxy.swift
@@ -160,7 +160,11 @@ public protocol FSProxy: AnyObject, Sendable {
     // FIXME: Need to document behavior w.r.t. error handling.
     func isDirectory(_ path: Path) -> Bool
 
-    /// Checks whether the given path has the execute bit (which on Windows is determined by the file extension).
+    /// Checks whether the given path is executable.
+    ///
+    /// On Windows, this is determined by the file extension (based on `SHGetFileInfo`), while on Unix it's determined by `access`, which means a file may be deemed executable even if no execute bit is set in the POSIX permissions.
+    ///
+    /// - seealso: [_stat, _stat32, _stat64, _stati64, _stat32i64, _stat64i32, _wstat, _wstat32, _wstat64, _wstati64, _wstat32i64, _wstat64i32](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/stat-functions)
     func isExecutable(_ path: Path) throws -> Bool
 
     /// Checks whether the given path is a symlink, also returning whether the linked file exists.
@@ -309,6 +313,10 @@ public extension FSProxy {
     func getFileSize(_ path: Path) throws -> ByteCount {
         try ByteCount(Int64(getFileInfo(path).size))
     }
+
+    func getFilePermissions(_ path: Path) throws -> FilePermissions {
+        try FilePermissions(rawValue: CModeT(getFilePermissions(path)))
+    }
 }
 
 fileprivate extension FSProxy {
@@ -853,7 +861,7 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
         }
 
         // Write the symlink.
-        directory.contents[path.basename] = Node(.symlink(target), permissions: 0o644, timestamp: getTimestamp(), inode: nextInode())
+        directory.contents[path.basename] = Node(.symlink(target), permissions: 0o755, timestamp: getTimestamp(), inode: nextInode())
         parent.timestamp = getTimestamp()
     }
 
@@ -1237,35 +1245,29 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
     public func getFileInfo(_ path: Path) throws -> FileInfo {
         return try queue.blocking_sync {
             guard let node = getNode(path) else { throw POSIXError(ENOENT) }
+
+            let type: FileAttributeType
+            let size: Int
             switch node.contents {
             case .file(let contents):
-                let info: [FileAttributeKey: any Sendable] = [
-                    .modificationDate : Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
-                    .type: FileAttributeType.typeRegular,
-                    .size: contents.bytes.count,
-                    .posixPermissions: 0,
-                    .systemNumber: node.device,
-                    .systemFileNumber: node.inode]
-                return createFileInfo(info)
+                type = .typeRegular
+                size = contents.bytes.count
             case .directory(let dir):
-                let info: [FileAttributeKey: any Sendable] = [
-                    .modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
-                    .type: FileAttributeType.typeDirectory,
-                    .size: dir.contents.count,
-                    .posixPermissions: 0,
-                    .systemNumber: node.device,
-                    .systemFileNumber: node.inode]
-                return createFileInfo(info)
-            case .symlink(_):
-                let info: [FileAttributeKey: any Sendable] = [
-                    .modificationDate: Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
-                    .type: FileAttributeType.typeSymbolicLink,
-                    .size: 0,
-                    .posixPermissions: 0,
-                    .systemNumber: node.device,
-                    .systemFileNumber: node.inode]
-                return createFileInfo(info)
+                type = .typeDirectory
+                size = dir.contents.count
+            case .symlink(let destination):
+                type = .typeSymbolicLink
+                size = destination.str.utf8.count
             }
+
+            let info: [FileAttributeKey: any Sendable] = [
+                .modificationDate : Date(timeIntervalSince1970: TimeInterval(node.timestamp)),
+                .type: type,
+                .size: size,
+                .posixPermissions: node.permissions,
+                .systemNumber: node.device,
+                .systemFileNumber: node.inode]
+            return createFileInfo(info)
         }
     }
 
diff --git a/Sources/SWBUtil/PbxCp.swift b/Sources/SWBUtil/PbxCp.swift
index f94fc7b7..a8dcd536 100644
--- a/Sources/SWBUtil/PbxCp.swift
+++ b/Sources/SWBUtil/PbxCp.swift
@@ -298,8 +298,9 @@ fileprivate func copyRegular(_ srcPath: Path, _ srcParentPath: Path, _ dstPath:
 
 func _copyFile(_ srcPath: Path, _ dstPath: Path) throws {
     do {
+        let existingPermissions: FilePermissions = try localFS.getFilePermissions(srcPath)
         var permissions: FilePermissions = [.ownerRead, .ownerWrite, .groupRead, .groupWrite, .otherRead, .otherWrite]
-        if try localFS.isExecutable(srcPath) {
+        if existingPermissions.contains(.ownerExecute) {
             permissions.insert([.ownerExecute, .groupExecute, .otherExecute])
         }
         let dstFd = try FileDescriptor.open(FilePath(dstPath.str), .writeOnly, options: [.create, .truncate], permissions: permissions)
diff --git a/Tests/SWBUtilTests/FSProxyTests.swift b/Tests/SWBUtilTests/FSProxyTests.swift
index 7a1aa90a..0389b852 100644
--- a/Tests/SWBUtilTests/FSProxyTests.swift
+++ b/Tests/SWBUtilTests/FSProxyTests.swift
@@ -350,11 +350,21 @@ import SWBTestSupport
             // Test setting file permissions.
             let execPath = tmpDir.join("script.sh")
             try localFS.write(execPath, contents: [])
+            #expect(try localFS.getFilePermissions(execPath) == 0o644)
+            #expect(try localFS.getFileInfo(execPath).permissions == 0o644)
             #expect(try !localFS.isExecutable(execPath))
-
             try localFS.setFilePermissions(execPath, permissions: 0o755)
             #expect(try localFS.getFilePermissions(execPath) == 0o755)
+            #expect(try localFS.getFileInfo(execPath).permissions == 0o755)
             #expect(try localFS.isExecutable(execPath))
+
+            let linkPath = tmpDir.join("script")
+            try localFS.symlink(linkPath, target: Path("script.sh"))
+            #expect(try localFS.getFilePermissions(linkPath) == 0o755)
+            #expect(try localFS.getFileInfo(linkPath).permissions == 0o755)
+            try localFS.setFilePermissions(linkPath, permissions: 0o644)
+            #expect(try localFS.getFilePermissions(linkPath) == 0o644)
+            #expect(try localFS.getFileInfo(linkPath).permissions == 0o644)
         }
     }
 
@@ -804,16 +814,28 @@ import SWBTestSupport
 
         // Test default permissions.
         #expect(try fs.getFilePermissions(.root) == 0o755)
+        #expect(try fs.getFileInfo(.root).permissions == 0o755)
         let filePath = Path.root.join("file.txt")
         try fs.write(filePath, contents: [])
         #expect(try fs.getFilePermissions(filePath) == 0o644)
+        #expect(try fs.getFileInfo(filePath).permissions == 0o644)
 
         // Test setting file permissions.
         let execPath = Path.root.join("script.sh")
         try fs.write(execPath, contents: [])
         #expect(try fs.getFilePermissions(execPath) == 0o644)
+        #expect(try fs.getFileInfo(execPath).permissions == 0o644)
         try fs.setFilePermissions(execPath, permissions: 0o755)
         #expect(try fs.getFilePermissions(execPath) == 0o755)
+        #expect(try fs.getFileInfo(execPath).permissions == 0o755)
+
+        let linkPath = Path.root.join("script")
+        try fs.symlink(linkPath, target: Path("script.sh"))
+        #expect(try fs.getFilePermissions(linkPath) == 0o755)
+        #expect(try fs.getFileInfo(linkPath).permissions == 0o755)
+        try fs.setFilePermissions(linkPath, permissions: 0o644)
+        #expect(try fs.getFilePermissions(linkPath) == 0o644)
+        #expect(try fs.getFileInfo(linkPath).permissions == 0o644)
     }
 
     @Test

From 9b8690fc41e86a4c9e0dbf3f7a97d7647b4778e8 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 15 Jul 2025 08:39:57 -0700
Subject: [PATCH 10/49] Fix hang in ServiceConsoleTests.serviceShutdown

This hang occurred only in CI environments and only on Linux. Here's the sequence of events:

- Test terminates swbuild using SIGKILL
- OS reparents SWBBuildService (a subprocess of swbuild) to launchd (Darwin) / init (others)
- OS closes the file descriptors for the I/O pipes swbuild has connected to SWBBuildService
- SWBBuildService's read() loop indicates EOF due to the broken pipe
- SWBBuildService causes itself to exit

At this point, the getpgid loop should return ERSCH and terminate the test. However, SWBBuildService is sticking around as a zombie for an extended period of time without init reaping the pid, causing getpgid to never hit the termination state. This causes the test to hang indefinitely.

To fix this, there are two aspects:

- A timeout is added around the termination monitoring loop that forces the exit promise to be fulfilled with an error if a 30-second interval elapses without the process exiting
- We switch from using a getpgid loop to using a waitid loop, where the terminal state is that the process has _exited_... we don't care if the zombie hasn't been collected by init, only that it's not in a running state

This fixes the hang for both the Jenkins based CI as well as GitHub actions, and also insulates us against future hangs by ensuring the test will terminate with a timeout error instead of hanging indefinitely, so that we at least know _which_ test is the problem.
---
 .../ConsoleCommands/ServiceConsoleTests.swift | 96 +++++++++++++++----
 1 file changed, 78 insertions(+), 18 deletions(-)

diff --git a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
index de9920d8..368a3667 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
@@ -19,7 +19,13 @@ import SWBUtil
 import WinSDK
 #endif
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+#if canImport(System)
+import System
+#else
+import SystemPackage
+#endif
+
+@Suite(.skipHostOS(.windows))
 fileprivate struct ServiceConsoleTests {
     @Test
     func emptyInput() async throws {
@@ -92,7 +98,13 @@ fileprivate struct ServiceConsoleTests {
             await #expect(try cli.exitStatus.wasSignaled)
 
             // Now wait for the service subprocess to exit, without any further communication.
-            try await serviceExitPromise.value
+            try await withTimeout(timeout: .seconds(30), description: "Service process exit promise 30-second limit") {
+                try await withTaskCancellationHandler {
+                    try await serviceExitPromise.value
+                } onCancel: {
+                    serviceExitPromise.fail(throwing: CancellationError())
+                }
+            }
         }
     }
 
@@ -124,6 +136,31 @@ private var SYNCHRONIZE: DWORD {
 }
 
 extension HANDLE: @retroactive @unchecked Sendable {}
+
+func WaitForSingleObjectAsync(_ handle: HANDLE) async throws {
+    var waitHandle: HANDLE?
+    defer {
+        if let waitHandle {
+            _ = UnregisterWait(waitHandle)
+        }
+    }
+
+    try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, any Error>) in
+        if !RegisterWaitForSingleObject(
+            &waitHandle,
+            handle,
+            { context, _ in
+                let continuation = Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! CheckedContinuation<Void, any Error>
+                continuation.resume()
+            },
+            Unmanaged.passRetained(continuation as AnyObject).toOpaque(),
+            INFINITE,
+            ULONG(WT_EXECUTEONLYONCE | WT_EXECUTELONGFUNCTION)
+        ) {
+            continuation.resume(throwing: Win32Error(GetLastError()))
+        }
+    }
+}
 #endif
 
 extension Processes {
@@ -134,24 +171,31 @@ extension Processes {
             throw Win32Error(GetLastError())
         }
         defer { CloseHandle(proc) }
-        Thread.detachNewThread {
-            if WaitForSingleObject(proc, INFINITE) == WAIT_FAILED {
-                promise.fail(throwing: Win32Error(GetLastError()))
-                return
-            }
-            promise.fulfill(with: ())
+        Task<Void, Never> {
+            await promise.fulfill(with: Result.catching { try await WaitForSingleObjectAsync(proc) })
         }
         #else
-        Task<Void, Never>.detached {
-            while true {
-                // We use getpgid() here to detect when the process has exited (it is not a child). Surprisingly, getpgid() is substantially faster than using kill(pid, 0) here because kill still returns success for zombies, and the service has been reparented to launchd. // ignore-unacceptable-language; POSIX API
-                do {
-                    if getpgid(pid) < 0 {
-                        // We expect the signal to eventually fail with "No such process".
-                        if errno != ESRCH {
-                            throw StubError.error("unexpected exit code: \(errno)")
+        Task<Void, Never> {
+            func wait(pid: pid_t) throws -> Bool {
+                repeat {
+                    do {
+                        var siginfo = siginfo_t()
+                        if waitid(P_PID, id_t(pid), &siginfo, WEXITED | WNOWAIT | WNOHANG) != 0 {
+                            throw Errno(rawValue: errno)
                         }
-                        break
+                        return siginfo.si_pid == pid
+                    } catch Errno.noChildProcess {
+                        return true
+                    } catch Errno.interrupted {
+                        // ignore
+                    }
+                } while true
+            }
+            while !Task.isCancelled {
+                do {
+                    if try wait(pid: pid) {
+                        promise.fulfill(with: ())
+                        return
                     }
                     try await Task.sleep(for: .microseconds(1000))
                 } catch {
@@ -159,9 +203,25 @@ extension Processes {
                     return
                 }
             }
-            promise.fulfill(with: ())
+            promise.fail(throwing: CancellationError())
         }
         #endif
         return promise
     }
 }
+
+#if !os(Windows) && !canImport(Darwin) && !os(FreeBSD)
+fileprivate extension siginfo_t {
+    var si_pid: pid_t {
+        #if os(OpenBSD)
+        return _data._proc._pid
+        #elseif canImport(Glibc)
+        return _sifields._sigchld.si_pid
+        #elseif canImport(Musl)
+        return __si_fields.__si_common.__first.__piduid.si_pid
+        #elseif canImport(Bionic)
+        return _sifields._kill._pid
+        #endif
+    }
+}
+#endif

From 3a6915efcc78d8538d05ae0925fe3cd110b46cc4 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 15 Jul 2025 08:39:57 -0700
Subject: [PATCH 11/49] Drop support for Focal in GitHub Actions CI

Per https://forums.swift.org/t/dropping-support-for-ubuntu-20-04/81109, the Swift project is dropping support for Ubuntu 20.04 (Focal) beginning in Swift 6.2 and later, so remove it from our testing matrix (it's failing now anyways).
---
 .github/workflows/pull_request.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 47ce72a9..c39112b4 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -13,9 +13,9 @@ jobs:
     name: Test
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
     with:
-      linux_os_versions: '["noble", "jammy", "focal", "rhel-ubi9"]'
+      linux_os_versions: '["noble", "jammy", "rhel-ubi9"]'
       linux_pre_build_command: |
-        if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy, focal
+        if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
           apt-get update -y
 
           # Build dependencies

From b79d617144e4e2af82ceddbddc7312f707ace665 Mon Sep 17 00:00:00 2001
From: Pavel Yaskevich <pyaskevich@apple.com>
Date: Sun, 27 Jul 2025 00:06:14 -0700
Subject: [PATCH 12/49] [SWUtil] PropertyListItem: Clarify use of `init`
 overloads that take array and dictionary

The original comment that these overloads can be removed was incorrect
because the existential of `PropertyListItemConvertible` doesn't self
conform. The overloads are still needed to support collections with
heterogeneous values but nothing else and so they should be disfavored.

Turns out that some of the expressions that used to take advantage
of these overloads only work due to the solver producing a valid
solution from the "diagnostic" mode.

For example:

```
public var propertyListItem: PropertyListItem {
    return .init(entries
        .sorted { $0.identifier < $1.identifier }
        .map {
            [
                "bundle-id": .plString($0.identifier),
                "tags": PropertyListItem($0.tags.sorted()),
                "bundle-path": .plString($0.path.str),
            ]
        })
}
```

This expression should have been ambiguous because it can match
both `.init(any ...)` and `.init([any ...])` overloads due to an
existential conversion that is bi-directional for the closure.

For `.init(any ...)`, result of the closure is going to be erased
to existential inside of the body of the `.map` closure and for
`.init([any ...])`, this erasure is going to happen outside but
both are equally valid.
---
 Sources/SWBUtil/PropertyList.swift | 14 +++++++++++---
 1 file changed, 11 insertions(+), 3 deletions(-)

diff --git a/Sources/SWBUtil/PropertyList.swift b/Sources/SWBUtil/PropertyList.swift
index 71cd3cb0..3e96dbdd 100644
--- a/Sources/SWBUtil/PropertyList.swift
+++ b/Sources/SWBUtil/PropertyList.swift
@@ -362,16 +362,24 @@ extension Dictionary: PropertyListItemConvertible where Key == String, Value: Pr
 }
 
 public extension PropertyListItem {
-    init(_ x: any PropertyListItemConvertible) {
+    init<T>(_ x: T) where T: PropertyListItemConvertible {
         self = x.propertyListItem
     }
 
-    // Will remove this with rdar://problem/44204273
+    /// This overload is intended only to support arrays with heterogeneous values.
+    /// For example: ["", 42] can be represented as [any PropertyListItemConvertible]
+    /// but `any PropertyListItemConvertible` doesn't actually conform to
+    /// `PropertyListItemConvertible` hence we cannot use the overload above.
+    @_disfavoredOverload
     init(_ x: [any PropertyListItemConvertible]) {
         self = .plArray(x.map{$0.propertyListItem})
     }
 
-    // Will remove this with rdar://problem/44204273
+    /// This overload is intended only to support dictionaries with heterogeneous values.
+    /// For example: ["q": "", "a": 42] can be represented as [String: any PropertyListItemConvertible]
+    /// but `any PropertyListItemConvertible` doesn't actually conform to
+    /// `PropertyListItemConvertible`.
+    @_disfavoredOverload
     init(_ x: [String: any PropertyListItemConvertible]) {
         self = .plDict(x.mapValues { $0.propertyListItem })
     }

From c6273fa11f4b7fe016f16e4536b6ef701974e809 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 15 Jul 2025 08:39:57 -0700
Subject: [PATCH 13/49] Fix incorrect error code propagation in
 CodeSignatureInfo.load

In the CodeSignatureInfo.load method, when SecCodeCopySigningInformation fails,
the code was incorrectly throwing MacError(result) instead of MacError(result2).
This caused misleading error messages since it propagated the error code from
the previous SecStaticCodeCreateWithPath call rather than the actual failing
SecCodeCopySigningInformation call.

The fix ensures accurate error reporting for code signing verification failures.
---
 Sources/SWBUtil/Signatures.swift | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/SWBUtil/Signatures.swift b/Sources/SWBUtil/Signatures.swift
index f9997d22..eb3c1e27 100644
--- a/Sources/SWBUtil/Signatures.swift
+++ b/Sources/SWBUtil/Signatures.swift
@@ -191,7 +191,7 @@ public struct CodeSignatureInfo: Codable, Sendable {
 
         let result2 = SecCodeCopySigningInformation(code!, [SecCSFlags(rawValue: kSecCSSigningInformation)], &info)
         if result2 != 0 {
-            throw MacError(result)
+            throw MacError(result2)
         }
 
         if !skipValidation {

From b233171111a78a870dc050228bd432b6c85f1960 Mon Sep 17 00:00:00 2001
From: Devin Coughlin <dcoughlin@apple.com>
Date: Thu, 24 Jul 2025 17:11:19 -0700
Subject: [PATCH 14/49] Add compiler build settings for Enhanced Security

Add a new "Security" build settings group and an
"Enhanced Security" build setting that enables a number of security
settings at once, including:
    * Enabling pointer authentication
    * Enabling the typed allocator
    * Enabling hardened libc++
    * Enabling stack zero initialization
    * Enabling security relevant compiler warnings

The goal is that enabling the Enhanced Security build setting
should enable all of these other settings by default and that
projects will be able to explicitly disable individual settings if needed.

rdar://151195113
---
 Sources/SWBCore/Specs/CoreBuildSystem.xcspec  |  27 +
 .../Specs/en.lproj/CoreBuildSystem.strings    |  29 +
 .../SWBUniversalPlatform/Specs/Clang.xcspec   | 164 ++++-
 Sources/SWBUniversalPlatform/Specs/Ld.xcspec  |   1 -
 .../Specs/en.lproj/Apple Clang.strings        |  17 +
 .../BuildOperationTests.swift                 |  84 ++-
 .../TaskConstructionTests.swift               | 608 +++++++++++++++++-
 7 files changed, 915 insertions(+), 15 deletions(-)

diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
index 0514414a..b1ad1195 100644
--- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
+++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
@@ -3535,6 +3535,33 @@ For more information on mergeable libraries, see [Configuring your project to us
                 Category = BuildOptions;
                 Description = "Enables building with code coverage instrumentation. This is only used when the build has code coverage enabled, which is typically done via the Xcode scheme or test plan settings.";
             },
+            {   Name = ENABLE_ENHANCED_SECURITY;
+                Type = Boolean;
+                DefaultValue = NO;
+                Category = SecurityPolicy;
+            },
+            {   Name = ENABLE_POINTER_AUTHENTICATION;
+                Type = Boolean;
+                DefaultValue = "$(ENABLE_ENHANCED_SECURITY)";
+                Category = SecurityPolicy;
+            },
+            {   Name = ENABLE_SECURITY_COMPILER_WARNINGS;
+                Type = Boolean;
+                DefaultValue = "$(ENABLE_ENHANCED_SECURITY)";
+                Category = SecurityPolicy;
+            },
+            {
+                Name = "ENABLE_C_BOUNDS_SAFETY";
+                Type = Boolean;
+                DefaultValue = NO;
+                Category = Security;
+            },
+            {
+                Name = "ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS";
+                Type = Boolean;
+                DefaultValue = NO;
+                Category = Security;
+            },
             {   Name = ENABLE_TESTABILITY;
                 Type = Boolean;
                 DefaultValue = NO;
diff --git a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
index 4aeafd33..d2482add 100644
--- a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
+++ b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
@@ -669,6 +669,35 @@ Typically this path is not set per target, but is provided as an option on the c
 "[MODULE_STOP]-name" = "Module Stop Routine";
 "[MODULE_STOP]-description" = "This defines the name of the kernel module stop routine. This is only used when building kernel extensions.";
 
+// Security Settings
+
+"[SecurityPolicy]-category" = "Security";
+
+"[ENABLE_ENHANCED_SECURITY]-name" = "Enable Enhanced Security";
+"[ENABLE_ENHANCED_SECURITY]-description" = "Enables a set of security build settings, including pointer authentication, typed allocator support, hardened C++ standard library, and security-related compiler warnings. These settings can be disabled individually.";
+"[ENABLE_ENHANCED_SECURITY]-value-[YES]" = "Yes";
+"[ENABLE_ENHANCED_SECURITY]-value-[NO]" = "No";
+
+"[ENABLE_POINTER_AUTHENTICATION]-name" = "Enable Pointer Authentication";
+"[ENABLE_POINTER_AUTHENTICATION]-description" = "Builds the target with pointer authentication enabled. Adds an additional architectural slice (arm64e) with pointer authentication instructions.";
+"[ENABLE_POINTER_AUTHENTICATION]-value-[YES]" = "Yes";
+"[ENABLE_POINTER_AUTHENTICATION]-value-[NO]" = "No";
+
+"[ENABLE_SECURITY_COMPILER_WARNINGS]-name" = "Enable Security-Relevant Compiler Warnings";
+"[ENABLE_SECURITY_COMPILER_WARNINGS]-description" = "Enables a set of security-relevant compiler warnings that check for common bounds-safety and lifetime-safety issues.";
+"[ENABLE_SECURITY_COMPILER_WARNINGS]-value-[YES]" = "Yes";
+"[ENABLE_SECURITY_COMPILER_WARNINGS]-value-[NO]" = "No";
+
+"[ENABLE_C_BOUNDS_SAFETY]-name" = "Enable Language Extension for Bounds Safety in C";
+"[ENABLE_C_BOUNDS_SAFETY]-description" = "Enables the -fbounds-safety language extension, which guarantees bounds safety for C";
+"[ENABLE_C_BOUNDS_SAFETY]-value-[NO]" = "No";
+"[ENABLE_C_BOUNDS_SAFETY]-value-[YES]" = "Yes";
+
+"[ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS]-name" = "Enforce Bounds-Safe Buffer Usage in C++";
+"[ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS]-description" = "Enables a strict programming model that guarantees bounds safety in C++ by rejecting raw pointer arithmetic (enabling the -Wunsafe-buffer-usage warning as an error) and requiring the use of hardened C++ Standard Library APIs for buffer manipulation.";
+"[ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS]-value-[NO]" = "No";
+"[ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS]-value-[YES]" = "Yes";
+
 // Compiler Version
 
 "[GCC_VERSION]-name" = "Compiler for C/C++/Objective-C";
diff --git a/Sources/SWBUniversalPlatform/Specs/Clang.xcspec b/Sources/SWBUniversalPlatform/Specs/Clang.xcspec
index f7ce248e..60e91aeb 100644
--- a/Sources/SWBUniversalPlatform/Specs/Clang.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/Clang.xcspec
@@ -536,7 +536,7 @@
                 FileTypes = (
                     "sourcecode.c.c",
                 );
-                DefaultValue = NO;
+                DefaultValue = "$(ENABLE_C_BOUNDS_SAFETY)";
                 CommandLineArgs = {
                     YES = ( "-fbounds-safety" );
                     NO  = ();
@@ -640,14 +640,74 @@
                 Category = LanguageCXX;
             },
             {
-                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_0;
+                Name = __ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES_YES;
+                Type = String;
+                DefaultValue = "YES";
+            },
+            {
+                Name = __ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES_NO;
+                Type = String;
+                DefaultValue = "YES";
+            },
+            {
+                Name = __ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_NO_YES;
+                Type = String;
+                DefaultValue = "YES";
+            },
+            {
+                Name = __ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_NO_NO;
+                Type = String;
+                DefaultValue = "NO";
+            },
+            {
+                Name = __ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS;
+                Type = String;
+                DefaultValue = "$(__ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_$(ENABLE_ENHANCED_SECURITY)_$(ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS))";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_0_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "debug";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_0_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_NO;
                 Type = String;
                 DefaultValue = "debug";
             },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_1_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_2_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_3_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_s_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_fast_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
+            {
+                Name = __LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_z_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_YES;
+                Type = String;
+                DefaultValue = "fast";
+            },
             {
                 Name = __LIBRARY_HARDENING_DEFAULT_VALUE;
                 Type = String;
-                DefaultValue = "$(__LIBRARY_HARDENING_DEFAULT_VALUE_$(GCC_OPTIMIZATION_LEVEL))";
+                DefaultValue = "$(__LIBRARY_HARDENING_DEFAULT_VALUE_OPT_LEVEL_$(GCC_OPTIMIZATION_LEVEL)_ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS_$(__ENHANCED_SECURITY_OR_BOUNDS_SAFE_BUFFERS))";
             },
             {
                 Name = "CLANG_CXX_STANDARD_LIBRARY_HARDENING";
@@ -736,11 +796,26 @@
                 );
                 Category = LanguageCXX;
             },
+            {
+                Name = "_UNSAFE_BUFFER_USAGE_DEFAULT_ENABLE_SAFE_BUFFERS_NO";
+                Type = String;
+                DefaultValue = "DEFAULT";
+            },
+            {
+                Name = "_UNSAFE_BUFFER_USAGE_DEFAULT_ENABLE_SAFE_BUFFERS_YES";
+                Type = String;
+                DefaultValue = "YES_ERROR";
+            },
+            {
+                Name = "_UNSAFE_BUFFER_USAGE_DEFAULT";
+                Type = String;
+                DefaultValue = "$(_UNSAFE_BUFFER_USAGE_DEFAULT_ENABLE_SAFE_BUFFERS_$(ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS))";
+            },
             {
                 Name = "CLANG_WARN_UNSAFE_BUFFER_USAGE";
                 Type = Enumeration;
                 Values = ( DEFAULT, YES, YES_ERROR, NO );
-                DefaultValue = DEFAULT;
+                DefaultValue = "$(_UNSAFE_BUFFER_USAGE_DEFAULT)";
                 CommandLineArgs = {
                     DEFAULT = ();
                     NO = ( "-Wno-unsafe-buffer-usage" );
@@ -801,7 +876,7 @@
             {
                 Name = "GCC_OPTIMIZATION_LEVEL";
                 Type = Enumeration;
-                // NOTE: Updating these values requires updating LLVM_OPTIMIZATION_LEVEL_VAL_X.
+                // NOTE: Updating these values requires updating LLVM_OPTIMIZATION_LEVEL_VAL_X and CLANG_CXX_STANDARD_LIBRARY_HARDENING.
                 Values = (
                     0,
                     1,
@@ -945,6 +1020,22 @@
                 };
                 Category = WarningsPolicy;
             },
+            {   Name = CLANG_ENABLE_SECURITY_COMPILER_WARNINGS;
+                Type = Boolean;
+                DefaultValue = "$(ENABLE_SECURITY_COMPILER_WARNINGS)";
+                CommandLineArgs = {
+                  YES = ("-Wbuiltin-memcpy-chk-size",
+                         "-Wformat-nonliteral",
+                         "-Warray-bounds",
+                         "-Warray-bounds-pointer-arithmetic",
+                         "-Wsuspicious-memaccess",
+                         "-Wsizeof-array-div",
+                         "-Wsizeof-pointer-div",
+                         "-Wreturn-stack-address");
+                  NO  = ();
+                };
+                // Hidden.
+            },
             {
                 Name = "GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS";
                 Type = Boolean;
@@ -1381,7 +1472,7 @@
             {
                 Name = "CLANG_WARN_EMPTY_BODY";
                 Type = Boolean;
-                DefaultValue = NO;
+                DefaultValue = "$(CLANG_ENABLE_SECURITY_COMPILER_WARNINGS)";
                 CommandLineArgs = {
                     YES = ( "-Wempty-body" );
                     NO = ( "-Wno-empty-body" );
@@ -1447,7 +1538,7 @@
             {
                 Name = "GCC_WARN_SHADOW";
                 Type = Boolean;
-                DefaultValue = NO;
+                DefaultValue = "$(CLANG_ENABLE_SECURITY_COMPILER_WARNINGS)";
                 CommandLineArgs = {
                     YES = (
                         "-Wshadow",
@@ -1800,6 +1891,27 @@
                 };
                 Category = Warnings;
             },
+            {
+                Name = "CLANG_ENABLE_STACK_ZERO_INIT";
+                Type = Boolean;
+                DefaultValue = "$(ENABLE_ENHANCED_SECURITY)";
+                Category = Security;
+            },
+            {
+                Name = "_CLANG_TRIVIAL_AUTO_VAR_INIT_DEFAULT_ENABLE_ZERO_INIT_YES";
+                Type = Boolean;
+                DefaultValue = "zero";
+            },
+            {
+                Name = "_CLANG_TRIVIAL_AUTO_VAR_INIT_DEFAULT_ENABLE_ZERO_INIT_NO";
+                Type = Boolean;
+                DefaultValue = "default";
+            },
+            {
+                Name = "_CLANG_TRIVIAL_AUTO_VAR_INIT_DEFAULT";
+                Type = String;
+                DefaultValue = "$(_CLANG_TRIVIAL_AUTO_VAR_INIT_DEFAULT_ENABLE_ZERO_INIT_$(CLANG_ENABLE_STACK_ZERO_INIT))";
+            },
             {
                 Name = "CLANG_TRIVIAL_AUTO_VAR_INIT";
                 Type = Enumeration;
@@ -1809,7 +1921,7 @@
                     zero,
                     pattern,
                 );
-                DefaultValue = default;
+                DefaultValue = "$(_CLANG_TRIVIAL_AUTO_VAR_INIT_DEFAULT)";
                 CommandLineArgs = {
                     default = ( );
                     uninitialized = ( "-ftrivial-auto-var-init=uninitialized" );
@@ -2900,7 +3012,17 @@
                 Condition = "$(CLANG_UNDEFINED_BEHAVIOR_SANITIZER_TRAP_ON_SECURITY_ISSUES) && $(GCC_OPTIMIZATION_LEVEL) != 0";
             },
             {
-                Name = "CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT";
+                Name = "_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT_ENHANCED_SECURITY_YES";
+                Type = Enumeration;
+                Values = (
+                    compiler-default,
+                    YES,
+                    NO,
+                );
+                DefaultValue = YES;
+            },
+            {
+                Name = "_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT_ENHANCED_SECURITY_NO";
                 Type = Enumeration;
                 Values = (
                     compiler-default,
@@ -2908,11 +3030,32 @@
                     NO,
                 );
                 DefaultValue = compiler-default;
+            },
+            {
+                Name = "_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT";
+                Type = Enumeration;
+                Values = (
+                    compiler-default,
+                    YES,
+                    NO,
+                );
+                DefaultValue = "$(_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT_ENHANCED_SECURITY_$(ENABLE_ENHANCED_SECURITY))";
+            },
+            {
+                Name = "CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT";
+                Type = Enumeration;
+                Values = (
+                    compiler-default,
+                    YES,
+                    NO,
+                );
+                DefaultValue = "$(_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT)";
                 CommandLineArgs = {
                   compiler-default = ();
                   YES = ("-ftyped-memory-operations");
                   NO  = ("-fno-typed-memory-operations");
                 };
+                Category = Language;
             },
             {
                 Name = "CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT";
@@ -2922,7 +3065,7 @@
                     YES,
                     NO,
                 );
-                DefaultValue = compiler-default;
+                DefaultValue = "$(_CLANG_ENABLE_TYPED_ALLOCATOR_SUPPORT_DEFAULT)";
                 CommandLineArgs = {
                   compiler-default = ();
                   YES = ("-ftyped-cxx-new-delete", "-ftyped-cxx-delete");
@@ -2933,6 +3076,7 @@
                     YES = ("-ftyped-cxx-new-delete", "-ftyped-cxx-delete");
                     NO = ();
                 };
+                Category = LanguageCXX;
             },
             // Index-while-building options, not visible in build settings.
             {
diff --git a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
index b39abe90..6a9d0d46 100644
--- a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
@@ -736,7 +736,6 @@
                     );
                 };
             },
-
             {
                 Name = "__CREATE_INFOPLIST_SECTION_IN_BINARY";
                 Type = Boolean;
diff --git a/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings b/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings
index 8a159c12..bd8b3100 100644
--- a/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings	
+++ b/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings	
@@ -695,6 +695,23 @@ The restrictions on `offsetof` may be relaxed in a future version of the C++ sta
 "[CLANG_ENABLE_MODULES]-value-[YES]" = "Yes";
 "[CLANG_ENABLE_MODULES]-value-[NO]" = "No";
 
+"[CLANG_ENABLE_STACK_ZERO_INIT]-name" = "Enable Stack Zero Initialization";
+"[CLANG_ENABLE_STACK_ZERO_INIT]-description" = "Automatically initializes stack variables to zero as a security protection.";
+"[CLANG_ENABLE_STACK_ZERO_INIT]-value-[YES]" = "Yes";
+"[CLANG_ENABLE_STACK_ZERO_INIT]-value-[NO]" = "No";
+
+"[CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT]-name" = "Enable Typed Allocator in C";
+"[CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT]-description" = "Enables compiler rewriting of allocation calls in C to provide type information to the allocator. Mitigates use-after-free security vulnerabilities.";
+"[CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT]-value-[YES]" = "Yes";
+"[CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT]-value-[NO]" = "No";
+"[CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT]-value-[compiler-default]" = "Compiler Default";
+
+"[CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT]-name" = "Enable Typed Allocator in C++";
+"[CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT]-description" = "Enables compiler rewriting of allocation calls in C++ to provide type information to the allocator. Mitigates use-after-free security vulnerabilities.";
+"[CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT]-value-[YES]" = "Yes";
+"[CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT]-value-[NO]" = "No";
+"[CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT]-value-[compiler-default]" = "Compiler Default";
+
 "[CLANG_MODULES_AUTOLINK]-name" = "Link Frameworks Automatically";
 "[CLANG_MODULES_AUTOLINK]-description" = "Automatically link SDK frameworks that are referenced using `#import` or `#include`. This feature requires also enabling support for modules. This build setting only applies to C-family languages.";
 "[CLANG_MODULES_AUTOLINK]-value-[YES]" = "Yes (when modules are enabled)";
diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
index 68dd3c20..869029c6 100644
--- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
@@ -6680,10 +6680,10 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
     }
 
     @Test(.requireSDKs(.iOS))
-    func pointerAuthenticationBuildSetting() async throws {
+    func pointerAuthenticationBuildSetting_iOS() async throws {
         func test(buildSettings: [String: String], expectedArchs: [String], line: UInt = #line) async throws {
 
-            try await withTemporaryDirectory { tmpDirPath async throws -> Void in
+            try await withTemporaryDirectory { (tmpDirPath: Path) async throws -> Void in
                 let testWorkspace = try await TestWorkspace(
                     "Test",
                     sourceRoot: tmpDirPath.join("Test"),
@@ -6719,7 +6719,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                     try await tester.fs.writeFileContents(swiftFile) { stream in }
                 }
 
-                try await tester.checkBuild(runDestination: .anyiOSDevice) { results in
+                try await tester.checkBuild(runDestination: .anyiOSDevice) { results -> Void in
                     results.checkNoErrors()
 
                     for arch in expectedArchs {
@@ -6727,6 +6727,7 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
                         results.checkTask(.matchRuleType("CompileC"), .matchRuleItemPattern(.suffix("File.m")), .matchRuleItem(arch)) { _ in }
                         results.checkTask(.matchRuleType("SwiftCompile"), .matchRuleItem(arch)) { _ in }
                         results.checkTask(.matchRuleType("SwiftEmitModule"), .matchRuleItem(arch)) { _ in }
+
                     }
                 }
             }
@@ -6734,6 +6735,83 @@ That command depends on command in Target 'agg2' (project \'aProject\'): script
 
         try await test(buildSettings: ["ENABLE_POINTER_AUTHENTICATION": "YES"], expectedArchs: ["arm64", "arm64e"])
         try await test(buildSettings: ["ENABLE_POINTER_AUTHENTICATION": "NO"], expectedArchs: ["arm64"])
+
+        // ENABLE_ENHANCED_SECURITY enables pointer authentication unless ENABLE_POINTER_AUTHENTICATION is explicitly disabled.
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "YES"], expectedArchs: ["arm64", "arm64e"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "NO"], expectedArchs: ["arm64"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "YES", "ENABLE_POINTER_AUTHENTICATION": "NO"], expectedArchs: ["arm64"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "NO", "ENABLE_POINTER_AUTHENTICATION": "YES"], expectedArchs: ["arm64e"])
+    }
+
+    @Test(.requireSDKs(.macOS))
+    func pointerAuthenticationBuildSetting_macOS() async throws {
+        func test(buildSettings: [String: String], expectedArchs: [String], line: UInt = #line) async throws {
+
+            try await withTemporaryDirectory { (tmpDirPath: Path) async throws -> Void in
+                let testWorkspace = try await TestWorkspace(
+                    "Test",
+                    sourceRoot: tmpDirPath.join("Test"),
+                    projects: [
+                        TestProject(
+                            "aProject",
+                            groupTree: TestGroup("Sources", children: [
+                                TestFile("File.c"),
+                                TestFile("File.swift"),
+                                TestFile("File.m"),
+                            ]),
+                            buildConfigurations: [TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "SDKROOT": "macosx",
+                                    "DONT_GENERATE_INFOPLIST_FILE": "YES",
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "SWIFT_VERSION": swiftVersion,
+                                ])],
+                            targets: [
+                                TestStandardTarget(
+                                    "aFramework", type: .framework,
+                                    buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: buildSettings)],
+                                    buildPhases: [
+                                        TestSourcesBuildPhase(["File.c", "File.swift", "File.m"]),
+                                    ]),
+                                TestStandardTarget(
+                                    "anApp", type: .application,
+                                    buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: buildSettings)],
+                                    buildPhases: [
+                                        TestSourcesBuildPhase(["File.c", "File.swift", "File.m"]),
+                                    ])
+                            ])])
+
+                let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+
+                // create the files
+                for file in ["File.swift", "File.c", "File.m"] {
+                    let swiftFile = testWorkspace.sourceRoot.join("aProject/\(file)")
+                    try await tester.fs.writeFileContents(swiftFile) { stream in }
+                }
+
+                try await tester.checkBuild(runDestination: .anyMac) { results -> Void in
+                    results.checkNoErrors()
+
+
+                    for arch in expectedArchs {
+                        results.checkTask(.matchRuleType("CompileC"), .matchRuleItemPattern(.suffix("File.c")), .matchRuleItem(arch)) { _ in }
+                        results.checkTask(.matchRuleType("CompileC"), .matchRuleItemPattern(.suffix("File.m")), .matchRuleItem(arch)) { _ in }
+                        results.checkTask(.matchRuleType("SwiftCompile"), .matchRuleItem(arch)) { _ in }
+                        results.checkTask(.matchRuleType("SwiftEmitModule"), .matchRuleItem(arch)) { _ in }
+                    }
+                }
+            }
+        }
+
+        try await test(buildSettings: ["ENABLE_POINTER_AUTHENTICATION": "YES"], expectedArchs: ["x86_64", "arm64", "arm64e"])
+        try await test(buildSettings: ["ENABLE_POINTER_AUTHENTICATION": "NO"], expectedArchs: ["x86_64", "arm64"])
+
+        // ENABLE_ENHANCED_SECURITY enables pointer authentication unless ENABLE_POINTER_AUTHENTICATION is explicitly disabled.
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "YES"], expectedArchs: ["x86_64", "arm64", "arm64e"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "NO"], expectedArchs: ["x86_64", "arm64"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "YES", "ENABLE_POINTER_AUTHENTICATION": "NO"], expectedArchs: ["x86_64", "arm64"])
+        try await test(buildSettings: ["ENABLE_ENHANCED_SECURITY": "NO", "ENABLE_POINTER_AUTHENTICATION": "YES"], expectedArchs: ["x86_64", "arm64e"])
     }
 
     @Test(.requireSDKs(.macOS))
diff --git a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
index d2c11ff1..ea146fcd 100644
--- a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
@@ -8428,6 +8428,82 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.macOS))
+    func zeroStackInit() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.c"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.c"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let enableStackZeroInitSetting = "CLANG_ENABLE_STACK_ZERO_INIT";
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+                if let val = overrides[enableStackZeroInitSetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-ftrivial-auto-var-init=zero"])
+                    } else if val == "NO" {
+                        task.checkCommandLineNoMatch([.contains("-ftrivial-auto-var-init")])
+                    }
+                } else if let val = overrides[enableEnhancedSecuritySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-ftrivial-auto-var-init=zero"])
+                    } else if val == "NO" {
+                        task.checkCommandLineNoMatch([.contains("-ftrivial-auto-var-init")])
+                    }
+                } else {
+                    task.checkCommandLineNoMatch([.contains("-ftrivial-auto-var-init")])
+                }
+            }
+
+            let overrides = [
+                [:],
+                [enableStackZeroInitSetting: "YES"],
+                [enableStackZeroInitSetting: "NO"],
+
+                [enableStackZeroInitSetting: "YES", enableEnhancedSecuritySetting: "YES"],
+                [enableStackZeroInitSetting: "YES", enableEnhancedSecuritySetting: "NO"],
+                [enableStackZeroInitSetting: "NO", enableEnhancedSecuritySetting: "YES"],
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
     @Test(.requireSDKs(.macOS))
     func typedMemoryOperations() async throws {
         try await withTemporaryDirectory { tmpDir in
@@ -8465,8 +8541,10 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
             let tester = try TaskConstructionTester(core, testProject)
             let typedMemoryOperationsC = "CLANG_ENABLE_C_TYPED_ALLOCATOR_SUPPORT";
             let typedMemoryOperationsCXX = "CLANG_ENABLE_CPLUSPLUS_TYPED_ALLOCATOR_SUPPORT";
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
 
             func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+
                 if let val = overrides[typedMemoryOperationsC] {
                     if val == "YES" {
                         task.checkCommandLineContains(["-ftyped-memory-operations"])
@@ -8487,6 +8565,16 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                         task.checkCommandLineDoesNotContain("-fno-typed-cxx-new-delete")
                         task.checkCommandLineDoesNotContain("-fno-typed-cxx-delete")
                     }
+                } else if let val = overrides[enableEnhancedSecuritySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-ftyped-memory-operations"])
+                        task.checkCommandLineContains(["-ftyped-cxx-new-delete", "-ftyped-cxx-delete"])
+                    } else if val == "NO" {
+                        task.checkCommandLineDoesNotContain("-ftyped-cxx-new-delete")
+                        task.checkCommandLineDoesNotContain("-ftyped-cxx-delete")
+                        task.checkCommandLineDoesNotContain("-fno-typed-cxx-new-delete")
+                        task.checkCommandLineDoesNotContain("-fno-typed-cxx-delete")
+                    }
                 }
             }
 
@@ -8497,6 +8585,22 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                 [typedMemoryOperationsCXX: "YES"],
                 [typedMemoryOperationsCXX: "NO"],
                 [typedMemoryOperationsCXX: "compiler-default"],
+
+                [enableEnhancedSecuritySetting: "YES"],
+                [enableEnhancedSecuritySetting: "NO"],
+                [enableEnhancedSecuritySetting: "NO", typedMemoryOperationsC: "NO"],
+                [enableEnhancedSecuritySetting: "NO", typedMemoryOperationsC: "YES"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsC: "NO"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsC: "YES"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsC: "compiler-default"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsC: "compiler-default"],
+
+                [enableEnhancedSecuritySetting: "NO", typedMemoryOperationsCXX: "NO"],
+                [enableEnhancedSecuritySetting: "NO", typedMemoryOperationsCXX: "YES"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsCXX: "NO"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsCXX: "YES"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsCXX: "compiler-default"],
+                [enableEnhancedSecuritySetting: "YES", typedMemoryOperationsCXX: "compiler-default"],
             ]
 
             for override in overrides {
@@ -8514,6 +8618,268 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.macOS))
+    func warnShadow() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.c"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.c"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let warnShadowSetting = "GCC_WARN_SHADOW";
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+                if let val = overrides[warnShadowSetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-Wshadow"])
+                    } else if (val == "NO") {
+                        task.checkCommandLineContains(["-Wno-shadow"])
+                    }
+                } else if let val = overrides[enableEnhancedSecuritySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-Wshadow"])
+                    } else if val == "NO" {
+                        task.checkCommandLineDoesNotContain("-Wshadow")
+                        task.checkCommandLineContains(["-Wno-shadow"])
+                    }
+                }
+            }
+
+            let overrides = [
+                [warnShadowSetting: "YES"],
+                [warnShadowSetting: "NO"],
+                [enableEnhancedSecuritySetting: "YES"],
+                [enableEnhancedSecuritySetting: "NO"],
+
+                [warnShadowSetting: "NO", enableEnhancedSecuritySetting: "YES"],
+                [warnShadowSetting: "YES", enableEnhancedSecuritySetting: "NO"],
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.macOS))
+    func warnEmptyBody() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.c"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.c"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let warnEmptyBodySetting = "CLANG_WARN_EMPTY_BODY";
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+                if let val = overrides[warnEmptyBodySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-Wempty-body"])
+                    } else if (val == "NO") {
+                        task.checkCommandLineContains(["-Wno-empty-body"])
+                    }
+                } else if let val = overrides[enableEnhancedSecuritySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-Wempty-body"])
+                    } else if val == "NO" {
+                        task.checkCommandLineDoesNotContain("-Wempty-body")
+                        task.checkCommandLineContains(["-Wno-empty-body"])
+                    }
+                }
+            }
+
+            let overrides = [
+                [warnEmptyBodySetting: "YES"],
+                [warnEmptyBodySetting: "NO"],
+                [enableEnhancedSecuritySetting: "YES"],
+                [enableEnhancedSecuritySetting: "NO"],
+
+                [warnEmptyBodySetting: "NO", enableEnhancedSecuritySetting: "YES"],
+                [warnEmptyBodySetting: "YES", enableEnhancedSecuritySetting: "NO"],
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.macOS))
+    func securityCompilerWarnings() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.c"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.c"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let enableSecurityCompilerWarningsSetting = "ENABLE_SECURITY_COMPILER_WARNINGS";
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
+
+            func generateWarningFlags(enabled: Bool) -> [String] {
+                let prefix = enabled ? "-W" : "-Wno-"
+                return ["\(prefix)builtin-memcpy-chk-size",
+                        "\(prefix)format-nonliteral",
+                        "\(prefix)array-bounds",
+                        "\(prefix)array-bounds-pointer-arithmetic",
+                        "\(prefix)suspicious-memaccess",
+                        "\(prefix)sizeof-array-div",
+                        "\(prefix)sizeof-pointer-div",
+                        "\(prefix)return-stack-address",]
+            }
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+
+                if let val = overrides[enableSecurityCompilerWarningsSetting] {
+                    if val == "YES" {
+                        for enabledWarning in generateWarningFlags(enabled: true) {
+                            task.checkCommandLineContains([enabledWarning])
+                        }
+                        for disabledWarning in generateWarningFlags(enabled: false) {
+                            task.checkCommandLineDoesNotContain(disabledWarning)
+                        }
+                    } else if val == "NO" {
+                        // Pass nothing on NO
+                        for enabledWarning in generateWarningFlags(enabled: true) {
+                            task.checkCommandLineDoesNotContain(enabledWarning)
+                        }
+                        for disabledWarning in generateWarningFlags(enabled: false) {
+                            task.checkCommandLineDoesNotContain(disabledWarning)
+                        }
+                    }
+                } else if let val = overrides[enableEnhancedSecuritySetting] {
+                    if val == "YES" {
+                        for enabledWarning in generateWarningFlags(enabled: true) {
+                            task.checkCommandLineContains([enabledWarning])
+                        }
+                        for disabledWarning in generateWarningFlags(enabled: false) {
+                            task.checkCommandLineDoesNotContain(disabledWarning)
+                        }
+                    } else if val == "NO" {
+                        // Pass nothing on NO
+                        for enabledWarning in generateWarningFlags(enabled: true) {
+                            task.checkCommandLineDoesNotContain(enabledWarning)
+                        }
+                        for disabledWarning in generateWarningFlags(enabled: false) {
+                            task.checkCommandLineDoesNotContain(disabledWarning)
+                        }
+                    }
+                }
+            }
+
+            let overrides = [
+                [enableSecurityCompilerWarningsSetting: "YES"],
+                [enableSecurityCompilerWarningsSetting: "NO"],
+
+                [enableEnhancedSecuritySetting: "YES"],
+                [enableEnhancedSecuritySetting: "NO"],
+                [enableEnhancedSecuritySetting: "NO", enableSecurityCompilerWarningsSetting: "NO"],
+                [enableEnhancedSecuritySetting: "NO", enableSecurityCompilerWarningsSetting: "YES"],
+                [enableEnhancedSecuritySetting: "YES", enableSecurityCompilerWarningsSetting: "NO"],
+                [enableEnhancedSecuritySetting: "YES", enableSecurityCompilerWarningsSetting: "YES"],
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
+
     @Test(.requireSDKs(.macOS))
     func unsafeBufferUsage() async throws {
         try await withTemporaryDirectory { tmpDir in
@@ -8549,7 +8915,10 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
 
             let core = try await getCore()
             let tester = try TaskConstructionTester(core, testProject)
-            let unsafeBufferUsageWarn = "CLANG_WARN_UNSAFE_BUFFER_USAGE";
+            let unsafeBufferUsageWarn = "CLANG_WARN_UNSAFE_BUFFER_USAGE"
+
+            // This setting enables both the CLANG_WARN_UNSAFE_BUFFER_USAGE and CLANG_CXX_STANDARD_LIBRARY_HARDENING
+            let enableBoundsSafeBuffers = "ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS"
 
             func test(task: any PlannedTask, overrides: [String: String]) -> Void {
                 if let val = overrides[unsafeBufferUsageWarn] {
@@ -8563,6 +8932,15 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                         task.checkCommandLineDoesNotContain("-Wunsafe-buffer-usage")
                         task.checkCommandLineDoesNotContain("-Wno-unsafe-buffer-usage")
                     }
+                } else if let val = overrides[enableBoundsSafeBuffers] {
+                    // When CLANG_WARN_UNSAFE_BUFFER_USAGE is not explicitly set, enable -Wunsafe-buffer-usage as on error when ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS is YES.
+                    if (val == "YES") {
+                        task.checkCommandLineContains(["-Werror=unsafe-buffer-usage"])
+                    } else if (val == "NO") {
+                        task.checkCommandLineDoesNotContain("-Wunsafe-buffer-usage")
+                        task.checkCommandLineDoesNotContain("-Wno-unsafe-buffer-usage")
+                        task.checkCommandLineDoesNotContain("-Werror=unsafe-buffer-usage")
+                    }
                 }
             }
 
@@ -8571,6 +8949,233 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                 [unsafeBufferUsageWarn: "NO"],
                 [unsafeBufferUsageWarn: "YES_ERROR"],
                 [unsafeBufferUsageWarn: "DEFAULT"],
+
+                [enableBoundsSafeBuffers: "YES"],
+                [enableBoundsSafeBuffers: "NO"],
+
+                [enableBoundsSafeBuffers: "YES", unsafeBufferUsageWarn: "NO"],
+                [enableBoundsSafeBuffers: "NO", unsafeBufferUsageWarn: "YES"],
+                [enableBoundsSafeBuffers: "NO", unsafeBufferUsageWarn: "YES_ERROR"],
+                [enableBoundsSafeBuffers: "NO", unsafeBufferUsageWarn: "YES_DEFAULT"],
+
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.macOS))
+    func boundsSafetyCLanguageExtension() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.c"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.c"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let enableBoundsSafetySetting = "CLANG_ENABLE_BOUNDS_SAFETY";
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+                if let val = overrides[enableBoundsSafetySetting] {
+                    if val == "YES" {
+                        task.checkCommandLineContains(["-fbounds-safety"])
+                    } else if (val == "NO") {
+                        task.checkCommandLineDoesNotContain("-fbounds-safety")
+                    }
+                }
+            }
+
+            let overrides = [
+                [enableBoundsSafetySetting: "YES"],
+                [enableBoundsSafetySetting: "NO"],
+            ]
+
+            for override in overrides {
+                await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: override), runDestination: .macOS, fs: fs) { results -> Void in
+                    results.checkTarget("AppTarget") { target -> Void in
+                        results.checkTask(.matchTarget(target), .matchRuleType("CompileC"), body: {task in test(task: task, overrides: override)})
+                    }
+                }
+            }
+        }
+    }
+
+
+    @Test(.requireSDKs(.macOS))
+    func libCPPLibraryHardening() async throws {
+        try await withTemporaryDirectory { tmpDir in
+            let testProject = TestProject(
+                "aProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles", path: "Sources",
+                    children: [
+                        TestFile("SourceFile.cpp"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug")
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "AppTarget",
+                        type: .application,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_INFOPLIST_FILE": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase([
+                                "SourceFile.cpp"
+                            ])
+                        ]
+                    )]
+            )
+
+            let fs = PseudoFS()
+
+            let core = try await getCore()
+            let tester = try TaskConstructionTester(core, testProject)
+            let cppHardeningSetting = "CLANG_CXX_STANDARD_LIBRARY_HARDENING"
+            let gccOptimizationLevelSetting = "GCC_OPTIMIZATION_LEVEL"
+
+            // When either ENABLE_ENHANCED_SECURITY or ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS are YES and CLANG_CXX_STANDARD_LIBRARY_HARDENING
+            // is not explicitly set, then fast hardening mode should be enabled for all but debug builds.
+            let enableEnhancedSecuritySetting = "ENABLE_ENHANCED_SECURITY"
+            let enableBoundsSafeBuffersSetting = "ENABLE_CPLUSPLUS_BOUNDS_SAFE_BUFFERS"
+
+            func test(task: any PlannedTask, overrides: [String: String]) -> Void {
+                enum HardeningMode {
+                    case NONE
+                    case FAST
+                    case EXTENSIVE
+                    case DEBUG
+                }
+
+                func checkCommandLineHardening(mode: HardeningMode, sourceLocation: SourceLocation = #_sourceLocation) {
+                    task.checkCommandLineContains(["-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_\(mode)"])
+                }
+
+                func checkCommandLineNoHardening(sourceLocation: SourceLocation = #_sourceLocation) {
+                    task.checkCommandLineNoMatch([.contains("-D_LIBCPP_HARDENING_MODE")])
+                }
+
+
+                if let cppHardeningSettingVal = overrides[cppHardeningSetting] {
+                    // When CLANG_CXX_STANDARD_LIBRARY_HARDENING is set, it wins and sets hardening mode policy
+                    switch cppHardeningSettingVal {
+                    case "":
+                        // When the value is not set, no hardening should be passed so that the compiler can determine the appropriate default
+                        // if any, based on the target triple.
+                        checkCommandLineNoHardening()
+                    case "none":
+                        checkCommandLineHardening(mode: .NONE)
+                    case "fast":
+                        checkCommandLineHardening(mode: .FAST)
+                    case "extensive":
+                        checkCommandLineHardening(mode: .EXTENSIVE)
+                    case "debug":
+                        checkCommandLineHardening(mode: .DEBUG)
+                    default:
+                        assertionFailure("Unexpected CLANG_CXX_STANDARD_LIBRARY_HARDENING override")
+                    }
+                } else {
+                    // When CLANG_CXX_STANDARD_LIBRARY_HARDENING is not set, the command-line depends on GCC_OPTIMIZATION_LEVEL and ENABLE_ENHANCED_SECURITY
+
+                    // If the optimization level is set and it is -O0, then the hardening is DEBUG.
+                    // Otherwise, the hardening level depends on ENABLE_ENHANCED_SECURITY. If enhanced security is enabled, then the default for non -O0 is FAST; otherwise
+                    // the default is to not set the hardening level.
+
+                    let isOptLevelZero = (overrides[gccOptimizationLevelSetting] == .some("0"))
+                    let isEnhancedSecurityEnabled = (overrides[enableEnhancedSecuritySetting] == .some("YES"))
+                    let isEnableBoundsSafeBuffersEnabled = (overrides[enableBoundsSafeBuffersSetting] == .some("YES"))
+
+                    if isOptLevelZero {
+                        checkCommandLineHardening(mode: .DEBUG)
+                    } else if isEnhancedSecurityEnabled || isEnableBoundsSafeBuffersEnabled {
+                        checkCommandLineHardening(mode: .FAST)
+                    } else {
+                        checkCommandLineNoHardening()
+                    }
+                }
+            }
+
+            let overrides = [
+                [gccOptimizationLevelSetting: "0"],
+                [gccOptimizationLevelSetting: "1"],
+                [gccOptimizationLevelSetting: "2"],
+                [gccOptimizationLevelSetting: "3"],
+                [gccOptimizationLevelSetting: "fast"],
+                [gccOptimizationLevelSetting: "s"],
+                [gccOptimizationLevelSetting: "z"],
+
+                [cppHardeningSetting: ""],
+                [cppHardeningSetting: "none"],
+                [cppHardeningSetting: "fast"],
+                [cppHardeningSetting: "extensive"],
+                [cppHardeningSetting: "debug"],
+
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "YES", cppHardeningSetting: "none"],
+
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "1", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "2", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "3", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "fast", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "s", enableEnhancedSecuritySetting: "YES"],
+                [gccOptimizationLevelSetting: "z", enableEnhancedSecuritySetting: "YES"],
+
+                [gccOptimizationLevelSetting: "0", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "1", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "2", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "3", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "fast", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "s", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "z", enableBoundsSafeBuffersSetting: "YES"],
+
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "YES", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "YES", enableBoundsSafeBuffersSetting: "NO"],
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "NO", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "0", enableEnhancedSecuritySetting: "NO", enableBoundsSafeBuffersSetting: "NO"],
+                [gccOptimizationLevelSetting: "s", enableEnhancedSecuritySetting: "YES", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "s", enableEnhancedSecuritySetting: "YES", enableBoundsSafeBuffersSetting: "NO"],
+                [gccOptimizationLevelSetting: "s", enableEnhancedSecuritySetting: "NO", enableBoundsSafeBuffersSetting: "YES"],
+                [gccOptimizationLevelSetting: "s", enableEnhancedSecuritySetting: "NO", enableBoundsSafeBuffersSetting: "NO"],
+
+                [enableEnhancedSecuritySetting: "YES"],
+                [enableBoundsSafeBuffersSetting: "YES"],
             ]
 
             for override in overrides {
@@ -8583,6 +9188,7 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
         }
     }
 
+
     @Test(.requireSDKs(.macOS))
     func warningSuppression() async throws {
         try await withTemporaryDirectory { tmpDir in

From 0c0eb26775e90c435d9885d2d75c0559c7401e9b Mon Sep 17 00:00:00 2001
From: Bassam Khouri <bkhouri@apple.com>
Date: Tue, 22 Jul 2025 16:34:55 -0400
Subject: [PATCH 15/49] Update minimum deployment target to macOS 14

---
 Package.swift | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/Package.swift b/Package.swift
index 42e0d705..f3d71a0f 100644
--- a/Package.swift
+++ b/Package.swift
@@ -70,7 +70,11 @@ func swiftSettings(languageMode: SwiftLanguageMode) -> [SwiftSetting] {
 let package = Package(
     name: "SwiftBuild",
     defaultLocalization: "en",
-    platforms: [.macOS("13.0"), .iOS("17.0"), .macCatalyst("17.0")],
+    platforms: [
+        .macOS(.v14),
+        .iOS("17.0"),
+        .macCatalyst("17.0"),
+    ],
     products: [
         .executable(name: "swbuild", targets: ["swbuild"]),
         .executable(name: "SWBBuildServiceBundle", targets: ["SWBBuildServiceBundle"]),

From c2a6449814f035429057c84c6be6c47f824d38ba Mon Sep 17 00:00:00 2001
From: Hamish Knight <hamish_knight@apple.com>
Date: Tue, 29 Jul 2025 22:19:09 +0100
Subject: [PATCH 16/49] Only check `isTargetSuitableForPlatformForIndex` for
 workspace description (#671)

* [test] NFC: Factor out `withHostToolsPackages`

Factor out the creation of the common package dependencies between
these two tests.

* [test] Parameterize two host tools index tests

Use Swift Testing parameterization to test different run destinations.

* [test] Set `SDKROOT: auto` at project level for `withHostToolsPackages`

This better matches what we actually generate for packages. Also
set all available platforms as supported.

* [test] Add same-package host tool dependency to `withHostToolsPackages`

This exposes the issue where `isTargetSuitableForPlatformForIndex`
returns `false` in the target and package build. Also change
`testHostToolsAndDependenciesAreBuiltDuringIndexingPreparationForPackage`
to use a dependency package and test both the target and package
build descriptions.

* Only check `isTargetSuitableForPlatformForIndex` for workspace description

For the package and target build description we can end up incorrectly
dropping build tool dependencies since not all clients pass in the
correct dependency information. It's not clear that we actually gain
much from doing this check for the target and package build
descriptions though, its primary purpose is to avoid configuring
unsupported targets in the workspace build description where we try
to configure for all available platforms. As such, switch to only
checking for the workspace build description. Note we don't encounter
this issue in the workspace case since there we override the build
parameters for host build tools to always build for the host platform.

rdar://152012769

* Remove dependency check from `isTargetSuitableForPlatformForIndex`

This gets applied pretty inconsistently, only 1 client actually
passes the correct dependency information. We ought to be able to rely
on the host platform being imposed for the workspace build description,
so it shouldn't be necessary.
---
 Sources/SWBCore/DependencyResolution.swift    |  32 +-
 .../SWBCore/LinkageDependencyResolver.swift   |  13 +-
 .../SWBCore/TargetDependencyResolver.swift    |   8 +-
 .../SWBTestSupport/BuildOperationTester.swift |   2 +
 .../HostBuildToolBuildOperationTests.swift    | 539 +++++++-----------
 .../IndexTargetDependencyResolverTests.swift  |   8 +-
 6 files changed, 230 insertions(+), 372 deletions(-)

diff --git a/Sources/SWBCore/DependencyResolution.swift b/Sources/SWBCore/DependencyResolution.swift
index 3316db1b..ccad7d11 100644
--- a/Sources/SWBCore/DependencyResolution.swift
+++ b/Sources/SWBCore/DependencyResolution.swift
@@ -623,26 +623,20 @@ extension SpecializationParameters {
 
     /// Determines whether a target should be configured for the given platform in the index arena.
     ///
-    /// The arena is used for two purposes:
-    ///   1. To retrieve settings for a given target
-    ///   2. To produce products of source dependencies for compilation purposes (it does not produce binaries)
+    /// When building a workspace build description, we configure for all possible platforms. As such,
+    /// we want to avoid unnecessarily configuring targets for unsupported platforms. When building a
+    /// target or package description, we are only configuring for a single platform and can therefore
+    /// avoid this check since we assume any dependency will necessary.
     ///
-    /// Thus, in general if a target doesn't support a platform, we don't want to configure it for that platform. If a
-    /// dependency is not supported for the platform of the dependent, presumably the dependent will not be able
-    /// to use its products for compilation purposes, since the source products will be put in a different platform
-    /// directory and/or they will not be usable by the dependent (e.g. the module will not be importable from a
-    /// different platform). If the dependency was intended to be usable from that platform for compilation purposes,
-    /// it would be a supported platform.
-    ///
-    /// There's an exception for this for a dependent host tool, which are required for compilation and must therefore
-    /// be configured (and registered as a dependency) regardless.
-    nonisolated func isTargetSuitableForPlatformForIndex(_ target: Target, parameters: BuildParameters, imposedParameters: SpecializationParameters?, dependencies: OrderedSet<ConfiguredTarget>? = nil) -> Bool {
-        if !buildRequest.enableIndexBuildArena {
-            return true
-        }
-
-        // Host tools case, always supported we'll override the parameters with that of the host regardless.
-        if target.isHostBuildTool || dependencies?.contains(where: { $0.target.isHostBuildTool }) == true {
+    /// Note there's an exception for this for host build tools, which are required for compilation
+    /// and must therefore be configured (and registered as a dependency) regardless
+    nonisolated func isTargetSuitableForPlatformForIndex(_ target: Target, parameters: BuildParameters, imposedParameters: SpecializationParameters?) -> Bool {
+        guard buildRequest.buildsIndexWorkspaceDescription else { return true }
+
+        // Host tools case, always supported since we'll override the parameters with that of the
+        // host regardless. Any dependencies will have the host platform imposed on them through
+        // `imposedParameters`.
+        if target.isHostBuildTool {
             return true
         }
 
diff --git a/Sources/SWBCore/LinkageDependencyResolver.swift b/Sources/SWBCore/LinkageDependencyResolver.swift
index e5b9fcd0..9c648201 100644
--- a/Sources/SWBCore/LinkageDependencyResolver.swift
+++ b/Sources/SWBCore/LinkageDependencyResolver.swift
@@ -122,8 +122,7 @@ actor LinkageDependencyResolver {
                 if Task.isCancelled { return }
                 let configuredTarget = topLevelTargetsToDiscover[i]
                 let imposedParameters = resolver.specializationParameters(configuredTarget, workspaceContext: workspaceContext, buildRequest: buildRequest, buildRequestContext: buildRequestContext)
-                let dependenciesOnPath = LinkageDependencies()
-                await linkageDependencies(for: configuredTarget, imposedParameters: imposedParameters, dependenciesOnPath: dependenciesOnPath)
+                await linkageDependencies(for: configuredTarget, imposedParameters: imposedParameters)
             }
         }
 
@@ -141,7 +140,7 @@ actor LinkageDependencyResolver {
     private var dependenciesPerTarget = [ConfiguredTarget: [ResolvedTargetDependency]]()
     private var visitedDiscoveredTargets = Set<ConfiguredTarget>()
 
-    private func linkageDependencies(for configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?, dependenciesOnPath: LinkageDependencies) async {
+    private func linkageDependencies(for configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?) async {
         // Track that we have visited this target.
         let visited = !visitedDiscoveredTargets.insert(configuredTarget).inserted
 
@@ -167,7 +166,7 @@ actor LinkageDependencyResolver {
                 return nil
             }
             let buildParameters = resolver.buildParametersByTarget[target] ?? configuredTarget.parameters
-            if await !resolver.isTargetSuitableForPlatformForIndex(target, parameters: buildParameters, imposedParameters: imposedParameters, dependencies: dependenciesOnPath.path) {
+            if await !resolver.isTargetSuitableForPlatformForIndex(target, parameters: buildParameters, imposedParameters: imposedParameters) {
                 return nil
             }
             let effectiveImposedParameters = imposedParameters?.effectiveParameters(target: configuredTarget, dependency: ConfiguredTarget(parameters: buildParameters, target: target), dependencyResolver: resolver)
@@ -195,7 +194,7 @@ actor LinkageDependencyResolver {
             } else {
                 imposedParametersForDependency = resolver.specializationParameters(dependency.target, workspaceContext: workspaceContext, buildRequest: buildRequest, buildRequestContext: buildRequestContext)
             }
-            await self.linkageDependencies(for: dependency.target, imposedParameters: imposedParametersForDependency, dependenciesOnPath: dependenciesOnPath)
+            await self.linkageDependencies(for: dependency.target, imposedParameters: imposedParametersForDependency)
         }
     }
 
@@ -657,7 +656,3 @@ private extension Path {
         return basenameWithoutSuffix.nilIfEmpty
     }
 }
-
-fileprivate actor LinkageDependencies {
-    var path: OrderedSet<ConfiguredTarget> = []
-}
diff --git a/Sources/SWBCore/TargetDependencyResolver.swift b/Sources/SWBCore/TargetDependencyResolver.swift
index 018abdc2..e1f85acb 100644
--- a/Sources/SWBCore/TargetDependencyResolver.swift
+++ b/Sources/SWBCore/TargetDependencyResolver.swift
@@ -680,7 +680,7 @@ fileprivate extension TargetDependencyResolver {
         }
 
         // Add the discovered info.
-        let discoveredInfo = await computeDiscoveredTargetInfo(for: configuredTarget, imposedParameters: imposedParameters, dependencyPath: nil, resolver: resolver)
+        let discoveredInfo = await computeDiscoveredTargetInfo(for: configuredTarget, imposedParameters: imposedParameters, resolver: resolver)
         discoveredTargets[configuredTarget] = discoveredInfo
 
         // If we have no dependencies, we are done.
@@ -742,7 +742,7 @@ fileprivate extension TargetDependencyResolver {
             discoveredInfo = info
         } else {
             if resolver.makeAggregateTargetsTransparentForSpecialization {
-                discoveredInfo = await computeDiscoveredTargetInfo(for: configuredTarget, imposedParameters: imposedParameters, dependencyPath: dependencyPath, resolver: resolver)
+                discoveredInfo = await computeDiscoveredTargetInfo(for: configuredTarget, imposedParameters: imposedParameters, resolver: resolver)
             } else {
                 var immediateDependencies = [ResolvedTargetDependency]()
                 var packageProductDependencies = [PackageProductTarget]()
@@ -820,14 +820,14 @@ fileprivate extension TargetDependencyResolver {
     }
 
     /// Discover the info for a configured target with the given imposed parameters.
-    private func computeDiscoveredTargetInfo(for configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?, dependencyPath: OrderedSet<ConfiguredTarget>?, resolver: isolated DependencyResolver) async -> DiscoveredTargetInfo {
+    private func computeDiscoveredTargetInfo(for configuredTarget: ConfiguredTarget, imposedParameters: SpecializationParameters?, resolver: isolated DependencyResolver) async -> DiscoveredTargetInfo {
         var immediateDependencies = [ResolvedTargetDependency]()
         var packageProductDependencies = [PackageProductTarget]()
         for dependency in resolver.explicitDependencies(for: configuredTarget) {
             if let asPackageProduct = dependency as? PackageProductTarget {
                 packageProductDependencies.append(asPackageProduct)
             } else {
-                if !resolver.isTargetSuitableForPlatformForIndex(dependency, parameters: configuredTarget.parameters, imposedParameters: imposedParameters, dependencies: dependencyPath) {
+                if !resolver.isTargetSuitableForPlatformForIndex(dependency, parameters: configuredTarget.parameters, imposedParameters: imposedParameters) {
                     continue
                 }
 
diff --git a/Sources/SWBTestSupport/BuildOperationTester.swift b/Sources/SWBTestSupport/BuildOperationTester.swift
index 5294b027..17c93778 100644
--- a/Sources/SWBTestSupport/BuildOperationTester.swift
+++ b/Sources/SWBTestSupport/BuildOperationTester.swift
@@ -1553,6 +1553,7 @@ package final class BuildOperationTester {
     /// Construct 'prepare' index build operation, and test the result.
     package func checkIndexBuild<T>(
         prepareTargets: [String],
+        buildTargets: [any TestTarget]? = nil,
         workspaceOperation: Bool = true,
         runDestination: RunDestinationInfo? = nil,
         persistent: Bool = false,
@@ -1561,6 +1562,7 @@ package final class BuildOperationTester {
     ) async throws -> T {
         let buildRequest = try Self.buildRequestForIndexOperation(
             workspace: workspace,
+            buildTargets: buildTargets,
             prepareTargets: prepareTargets,
             workspaceOperation: workspaceOperation,
             runDestination: runDestination,
diff --git a/Tests/SWBBuildSystemTests/HostBuildToolBuildOperationTests.swift b/Tests/SWBBuildSystemTests/HostBuildToolBuildOperationTests.swift
index 1e4748d3..689bf872 100644
--- a/Tests/SWBBuildSystemTests/HostBuildToolBuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/HostBuildToolBuildOperationTests.swift
@@ -158,198 +158,119 @@ fileprivate struct HostBuildToolBuildOperationTests: CoreBasedTests {
         }
     }
 
-    @Test(.requireSDKs(.macOS))
-    func hostToolsAndDependenciesAreBuiltDuringIndexingPreparation_Mac() async throws {
-        try await testHostToolsAndDependenciesAreBuiltDuringIndexingPreparation(destination: .anyMac)
-    }
-
-    @Test(.requireSDKs(.macOS, .iOS))
-    func hostToolsAndDependenciesAreBuiltDuringIndexingPreparation_MacCatalyst() async throws {
-        try await testHostToolsAndDependenciesAreBuiltDuringIndexingPreparation(destination: .anyMacCatalyst)
-    }
-
-    @Test(.requireSDKs(.macOS, .iOS))
-    func hostToolsAndDependenciesAreBuiltDuringIndexingPreparation_iOS() async throws {
-        try await testHostToolsAndDependenciesAreBuiltDuringIndexingPreparation(destination: .anyiOSDevice)
-    }
-
-    func testHostToolsAndDependenciesAreBuiltDuringIndexingPreparation(destination: RunDestinationInfo) async throws {
-        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
-            let depPackage = try await TestPackageProject(
-                "DepPackage",
-                groupTree: TestGroup("Foo", children: [
-                    TestFile("transitivedep.swift"),
-                    TestFile("dep.swift"),
-                ]),
-                buildConfigurations: [
-                    TestBuildConfiguration(
-                        "Debug",
-                        buildSettings: [
-                            "SWIFT_VERSION": swiftVersion,
-                            "GENERATE_INFOPLIST_FILE": "YES",
-                            "PRODUCT_NAME": "$(TARGET_NAME)",
-                            "CODE_SIGNING_ALLOWED": "NO",
-                        ]),
-                ],
-                targets: [
-                    TestStandardTarget("TransitivePackageDep", type: .objectFile, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ],
-                            impartedBuildProperties:
-                                TestImpartedBuildProperties(
-                                    buildSettings: [
-                                        "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "IMPARTED_SETTINGS"
-                                    ])
-                        ),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["transitivedep.swift"])
+    private func withHostToolsPackages(
+        clients: TestProject...,
+        body: (BuildOperationTester, TestWorkspace) async throws -> Void
+    ) async throws {
+        let depPackage = try await TestPackageProject(
+            "DepPackage",
+            groupTree: TestGroup("Foo", children: [
+                TestFile("transitivedep.swift"),
+                TestFile("dep.swift"),
+            ]),
+            buildConfigurations: [
+                TestBuildConfiguration(
+                    "Debug",
+                    buildSettings: [
+                        "SWIFT_VERSION": swiftVersion,
+                        "GENERATE_INFOPLIST_FILE": "YES",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "CODE_SIGNING_ALLOWED": "NO",
+                        "SDKROOT": "auto",
+                        "SUPPORTED_PLATFORMS": "$(AVAILABLE_PLATFORMS)",
                     ]),
-                    TestStandardTarget("PackageDep", type: .staticLibrary, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["dep.swift"]),
-                        TestFrameworksBuildPhase([
-                            TestBuildFile(.target("TransitivePackageDep"))
-                        ])
-                    ], dependencies: [
-                        "TransitivePackageDep"
-                    ]),
-                    TestPackageProductTarget("PackageDepProduct", frameworksBuildPhase:
-                        TestFrameworksBuildPhase([
-                            TestBuildFile(.target("PackageDep")),
-                        ]
-                    ), buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], dependencies: [
-                        "PackageDep"
-                    ]),
-            ])
-
-            let hostToolsPackage = try await TestPackageProject(
-                "HostToolsPackage",
-                groupTree: TestGroup("Foo", children: [
-                    TestFile("tool.swift"),
-                    TestFile("lib.swift"),
-                ]),
-                buildConfigurations: [
+            ],
+            targets: [
+                TestStandardTarget("TransitivePackageDep", type: .objectFile, buildConfigurations: [
                     TestBuildConfiguration(
                         "Debug",
-                        buildSettings: [
-                            "SWIFT_VERSION": swiftVersion,
-                            "GENERATE_INFOPLIST_FILE": "YES",
-                            "PRODUCT_NAME": "$(TARGET_NAME)",
-                            "CODE_SIGNING_ALLOWED": "NO",
-                        ]),
-                ],
-                targets: [
-                    TestStandardTarget("HostTool", type: .hostBuildTool, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                            ])
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["tool.swift"]),
-                        TestFrameworksBuildPhase([TestBuildFile(.target("PackageDepProduct"))])
-                    ], dependencies: [
-                        "PackageDepProduct"
-                    ]),
-                    TestStandardTarget("HostToolClientLib", type: .objectFile, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["lib.swift"]),
-                    ], dependencies: [
-                        "HostTool"
+                        buildSettings: [:],
+                        impartedBuildProperties:
+                            TestImpartedBuildProperties(
+                                buildSettings: [
+                                    "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "IMPARTED_SETTINGS"
+                                ])
+                    ),
+                ], buildPhases: [
+                    TestSourcesBuildPhase(["transitivedep.swift"])
+                ]),
+                TestStandardTarget("PackageDep", type: .staticLibrary, buildPhases: [
+                    TestSourcesBuildPhase(["dep.swift"]),
+                    TestFrameworksBuildPhase([
+                        TestBuildFile(.target("TransitivePackageDep"))
+                    ])
+                ], dependencies: [
+                    "TransitivePackageDep"
+                ]),
+                TestPackageProductTarget("PackageDepProduct", frameworksBuildPhase:
+                    TestFrameworksBuildPhase([
+                        TestBuildFile(.target("PackageDep")),
+                    ]
+                ), dependencies: [
+                    "PackageDep"
+                ]),
+        ])
+
+        let hostToolsPackage = try await TestPackageProject(
+            "HostToolsPackage",
+            groupTree: TestGroup("Foo", children: [
+                TestFile("tooldep.swift"),
+                TestFile("tool.swift"),
+                TestFile("lib.swift"),
+            ]),
+            buildConfigurations: [
+                TestBuildConfiguration(
+                    "Debug",
+                    buildSettings: [
+                        "SWIFT_VERSION": swiftVersion,
+                        "GENERATE_INFOPLIST_FILE": "YES",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "CODE_SIGNING_ALLOWED": "NO",
+                        "SDKROOT": "auto",
+                        "SUPPORTED_PLATFORMS": "$(AVAILABLE_PLATFORMS)",
                     ]),
-                    TestPackageProductTarget("HostToolClientLibProduct", frameworksBuildPhase:
-                        TestFrameworksBuildPhase([TestBuildFile(.target("HostToolClientLib"))]
-                    ), buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], dependencies: [
-                        "HostToolClientLib"
+            ],
+            targets: [
+                TestStandardTarget("HostToolDep", type: .objectFile, buildPhases: [
+                    TestSourcesBuildPhase(["tooldep.swift"]),
+                ]),
+                TestStandardTarget("HostTool", type: .hostBuildTool, buildPhases: [
+                    TestSourcesBuildPhase(["tool.swift"]),
+                    TestFrameworksBuildPhase([
+                        TestBuildFile(.target("PackageDepProduct")),
+                        TestBuildFile(.target("HostToolDep")),
                     ]),
-            ])
+                ], dependencies: [
+                    "PackageDepProduct",
+                    "HostToolDep",
+                ]),
+                TestStandardTarget("HostToolClientLib", type: .objectFile, buildPhases: [
+                    TestSourcesBuildPhase(["lib.swift"]),
+                ], dependencies: [
+                    "HostTool"
+                ]),
+                TestPackageProductTarget("HostToolClientLibProduct", frameworksBuildPhase:
+                    TestFrameworksBuildPhase([TestBuildFile(.target("HostToolClientLib"))]
+                ), dependencies: [
+                    "HostToolClientLib"
+                ]),
+        ])
 
-            let testProject = try await TestProject(
-                "aProject",
-                groupTree: TestGroup("Foo", children: [
-                    TestFile("frame.swift"),
-                    TestFile("app.swift")
-                ]), buildConfigurations: [
-                    TestBuildConfiguration(
-                        "Debug",
-                        buildSettings: [
-                            "SWIFT_VERSION": swiftVersion,
-                            "GENERATE_INFOPLIST_FILE": "YES",
-                            "PRODUCT_NAME": "$(TARGET_NAME)",
-                            "CODE_SIGNING_ALLOWED": "NO",
-                        ]),
-                ],
-                targets: [
-                    TestStandardTarget("Framework", type: .framework, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["frame.swift"]),
-                        TestFrameworksBuildPhase([
-                            TestBuildFile(.target("HostToolClientLibProduct"))
-                        ]),
-                    ], dependencies: [
-                        "HostToolClientLibProduct"
-                    ]),
-                    TestStandardTarget("App", type: .application, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["app.swift"]),
-                    ], dependencies: [
-                        "Framework"
-                    ]),
-                ]
-            )
-            let testWorkspace = TestWorkspace("aWorkspace", sourceRoot: tmpDirPath.join("Test"), projects: [depPackage, hostToolsPackage, testProject])
+        try await withTemporaryDirectory { tmpDirPath in
+            let testWorkspace = TestWorkspace("aWorkspace", sourceRoot: tmpDirPath.join("Test"), projects: [depPackage, hostToolsPackage] + clients)
             let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false, systemInfo: .init(operatingSystemVersion: Version(99, 98, 97), productBuildVersion: "99A98", nativeArchitecture: Architecture.host.stringValue ?? "undefined_arch"))
 
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("DepPackage/transitivedep.swift")) { stream in
+            let fs = tester.fs
+            let root = testWorkspace.sourceRoot
+
+            try await fs.writeFileContents(root.join("DepPackage/transitivedep.swift")) { stream in
                 stream <<<
                 """
                 public let transitiveDependencyMessage = "Hello from host tool transitive dependency!"
                 """
             }
 
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("DepPackage/dep.swift")) { stream in
+            try await fs.writeFileContents(root.join("DepPackage/dep.swift")) { stream in
                 stream <<<
                 """
                 import TransitivePackageDep
@@ -361,26 +282,87 @@ fileprivate struct HostBuildToolBuildOperationTests: CoreBasedTests {
                 """
             }
 
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("HostToolsPackage/tool.swift")) { stream in
+            try await fs.writeFileContents(root.join("HostToolsPackage/tooldep.swift")) { stream in
+                stream <<<
+                """
+                public let samePackageMsg = "Hello from host tool same-package dependency!"
+                """
+            }
+
+            try await fs.writeFileContents(root.join("HostToolsPackage/tool.swift")) { stream in
                 stream <<<
                 """
                 import PackageDep
+                import HostToolDep
 
                 @main struct Foo {
                     static func main() {
-                        print("Hello from host tool! " + dependencyMessage)
+                        print("Hello from host tool! " + dependencyMessage + samePackageMsg)
                     }
                 }
                 """
             }
 
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("HostToolsPackage/lib.swift")) { stream in
+            try await fs.writeFileContents(root.join("HostToolsPackage/lib.swift")) { stream in
                 stream <<<
                 """
                 public class MyClass {}
                 """
             }
 
+            try await body(tester, testWorkspace)
+        }
+    }
+
+    @Test(.requireSDKs(.macOS, .iOS), arguments: [RunDestinationInfo.anyMac, .anyMacCatalyst, .anyiOSDevice])
+    func testHostToolsAndDependenciesAreBuiltDuringIndexingPreparation(destination: RunDestinationInfo) async throws {
+        let testProject = try await TestProject(
+            "aProject",
+            groupTree: TestGroup("Foo", children: [
+                TestFile("frame.swift"),
+                TestFile("app.swift")
+            ]), buildConfigurations: [
+                TestBuildConfiguration(
+                    "Debug",
+                    buildSettings: [
+                        "SWIFT_VERSION": swiftVersion,
+                        "GENERATE_INFOPLIST_FILE": "YES",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "CODE_SIGNING_ALLOWED": "NO",
+                    ]),
+            ],
+            targets: [
+                TestStandardTarget("Framework", type: .framework, buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "SDKROOT": "auto",
+                            "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
+                        ]),
+                ], buildPhases: [
+                    TestSourcesBuildPhase(["frame.swift"]),
+                    TestFrameworksBuildPhase([
+                        TestBuildFile(.target("HostToolClientLibProduct"))
+                    ]),
+                ], dependencies: [
+                    "HostToolClientLibProduct"
+                ]),
+                TestStandardTarget("App", type: .application, buildConfigurations: [
+                    TestBuildConfiguration(
+                        "Debug",
+                        buildSettings: [
+                            "SDKROOT": "auto",
+                            "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
+                        ]),
+                ], buildPhases: [
+                    TestSourcesBuildPhase(["app.swift"]),
+                ], dependencies: [
+                    "Framework"
+                ]),
+            ]
+        )
+
+        try await withHostToolsPackages(clients: testProject) { tester, testWorkspace in
             try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("aProject/frame.swift")) { stream in
                 stream <<<
                 """
@@ -439,176 +421,57 @@ fileprivate struct HostBuildToolBuildOperationTests: CoreBasedTests {
         }
     }
 
-    @Test(.requireSDKs(.macOS))
-    func testHostToolsAndDependenciesAreBuiltDuringIndexingPreparationForPackage() async throws {
-        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
-            let depPackage = try await TestPackageProject(
-                "DepPackage",
-                groupTree: TestGroup("Foo", children: [
-                    TestFile("transitivedep.swift"),
-                    TestFile("dep.swift"),
-                ]),
-                buildConfigurations: [
-                    TestBuildConfiguration(
-                        "Debug",
-                        buildSettings: [
-                            "SWIFT_VERSION": swiftVersion,
-                            "GENERATE_INFOPLIST_FILE": "YES",
-                            "PRODUCT_NAME": "$(TARGET_NAME)",
-                            "CODE_SIGNING_ALLOWED": "NO",
-                        ]),
-                ],
-                targets: [
-                    TestStandardTarget("TransitivePackageDep", type: .objectFile, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ],
-                            impartedBuildProperties:
-                                TestImpartedBuildProperties(
-                                    buildSettings: [
-                                        "SWIFT_ACTIVE_COMPILATION_CONDITIONS": "IMPARTED_SETTINGS"
-                                    ])
-                        ),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["transitivedep.swift"])
+    @Test(.requireSDKs(.macOS, .iOS), arguments: [RunDestinationInfo.anyMac, .anyMacCatalyst, .anyiOSDevice], [true, false])
+    func testHostToolsAndDependenciesAreBuiltDuringIndexingPreparationForPackage(
+        destination: RunDestinationInfo, targetBuild: Bool
+    ) async throws {
+        let clientPackage = try await TestPackageProject(
+            "ClientPackage",
+            groupTree: TestGroup("Client", children: [
+                TestFile("main.swift"),
+            ]),
+            buildConfigurations: [
+                TestBuildConfiguration(
+                    "Debug",
+                    buildSettings: [
+                        "SWIFT_VERSION": swiftVersion,
+                        "GENERATE_INFOPLIST_FILE": "YES",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "CODE_SIGNING_ALLOWED": "NO",
+                        "SDKROOT": "auto",
+                        "SUPPORTED_PLATFORMS": "$(AVAILABLE_PLATFORMS)",
                     ]),
-                    TestStandardTarget("PackageDep", type: .staticLibrary, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["dep.swift"]),
-                        TestFrameworksBuildPhase([
-                            TestBuildFile(.target("TransitivePackageDep"))
-                        ])
-                    ], dependencies: [
-                        "TransitivePackageDep"
-                    ]),
-                    TestPackageProductTarget("PackageDepProduct", frameworksBuildPhase:
-                        TestFrameworksBuildPhase([
-                            TestBuildFile(.target("PackageDep")),
-                        ]
-                    ), buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], dependencies: [
-                        "PackageDep"
-                    ]),
-            ])
-
-            let hostToolsPackage = try await TestPackageProject(
-                "HostToolsPackage",
-                groupTree: TestGroup("Foo", children: [
-                    TestFile("tool.swift"),
-                    TestFile("lib.swift"),
+            ],
+            targets: [
+                TestStandardTarget("HostToolClient", type: .objectFile, buildPhases: [
+                    TestSourcesBuildPhase(["main.swift"]),
+                    TestFrameworksBuildPhase([TestBuildFile(.target("HostToolClientLibProduct"))]),
+                ], dependencies: [
+                    "HostToolClientLibProduct"
                 ]),
-                buildConfigurations: [
-                    TestBuildConfiguration(
-                        "Debug",
-                        buildSettings: [
-                            "SWIFT_VERSION": swiftVersion,
-                            "GENERATE_INFOPLIST_FILE": "YES",
-                            "PRODUCT_NAME": "$(TARGET_NAME)",
-                            "CODE_SIGNING_ALLOWED": "NO",
-                        ]),
-                ],
-                targets: [
-                    TestStandardTarget("HostTool", type: .hostBuildTool, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                            ])
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["tool.swift"]),
-                        TestFrameworksBuildPhase([TestBuildFile(.target("PackageDepProduct"))])
-                    ], dependencies: [
-                        "PackageDepProduct"
-                    ]),
-                    TestStandardTarget("HostToolClientLib", type: .objectFile, buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], buildPhases: [
-                        TestSourcesBuildPhase(["lib.swift"]),
-                    ], dependencies: [
-                        "HostTool"
-                    ]),
-                    TestPackageProductTarget("HostToolClientLibProduct", frameworksBuildPhase:
-                        TestFrameworksBuildPhase([TestBuildFile(.target("HostToolClientLib"))]
-                    ), buildConfigurations: [
-                        TestBuildConfiguration(
-                            "Debug",
-                            buildSettings: [
-                                "SDKROOT": "auto",
-                                "SUPPORTED_PLATFORMS": "macosx iphoneos iphonesimulator",
-                            ]),
-                    ], dependencies: [
-                        "HostToolClientLib"
-                    ]),
-            ])
-
-            let testWorkspace = TestWorkspace("aWorkspace", sourceRoot: tmpDirPath.join("Test"), projects: [depPackage, hostToolsPackage])
-            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false, systemInfo: .init(operatingSystemVersion: Version(99, 98, 97), productBuildVersion: "99A98", nativeArchitecture: Architecture.host.stringValue ?? "undefined_arch"))
-
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("DepPackage/transitivedep.swift")) { stream in
-                stream <<<
-                """
-                public let transitiveDependencyMessage = "Hello from host tool transitive dependency!"
-                """
-            }
+        ])
 
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("DepPackage/dep.swift")) { stream in
+        try await withHostToolsPackages(clients: clientPackage) { tester, testWorkspace in
+            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("ClientPackage/main.swift")) { stream in
                 stream <<<
                 """
-                import TransitivePackageDep
-
-                public let dependencyMessage = "Hello from host tool dependency! " + transitiveDependencyMessage
-                #if !IMPARTED_SETTINGS
-                #error("settings not imparted")
-                #endif
-                """
-            }
-
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("HostToolsPackage/tool.swift")) { stream in
-                stream <<<
-                """
-                import PackageDep
-
-                @main struct Foo {
-                    static func main() {
-                        print("Hello from host tool! " + dependencyMessage)
-                    }
-                }
-                """
-            }
-
-            try await tester.fs.writeFileContents(testWorkspace.sourceRoot.join("HostToolsPackage/lib.swift")) { stream in
-                stream <<<
-                """
-                public class MyClass {}
+                print("Hello, world!")
                 """
             }
 
-            try await tester.checkIndexBuild(prepareTargets: hostToolsPackage.targets.map(\.guid), workspaceOperation: false, runDestination: .anyMac, persistent: true) { results in
+            let clientTarget = try #require(clientPackage.targets.first)
+            try await tester.checkIndexBuild(
+                prepareTargets: [clientTarget.guid],
+                buildTargets: targetBuild ? [clientTarget] : nil,
+                workspaceOperation: false, runDestination: destination,
+                persistent: true
+            ) { results in
                 results.checkNoDiagnostics()
 
                 results.checkTaskExists(.matchTargetName("HostTool"), .matchRuleType("Ld"))
                 try results.checkTask(.matchTargetName("HostTool"), .matchRuleType(ProductPlan.preparedForIndexPreCompilationRuleName)) { task in
                     try results.checkTaskFollows(task, .matchTargetName("PackageDep"), .matchRuleType("Libtool"))
+                    try results.checkTaskFollows(task, .matchTargetName("HostToolDep"), .matchRuleType("SwiftDriver Compilation"))
                 }
             }
         }
diff --git a/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift b/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
index c787f3f7..72009cc8 100644
--- a/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
+++ b/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
@@ -949,8 +949,12 @@ import SWBUtil
             #expect(results.targets(packageProduct).map{ results.targetNameAndPlatform($0) } == ["PackageLibProduct-iphoneos"])
             #expect(results.targets(unreferencedPackageLib).map{ results.targetNameAndPlatform($0) } == ["UnreferencedPackageLib-iphoneos"])
 
-            try results.checkDependencies(of: .init(packageLib2, "iphoneos"), are: [])
-
+            switch results.graphType {
+            case .dependency:
+                try results.checkDependencies(of: packageLib2, are: [.init(packageTool, "macos")])
+            case .linkage:
+                try results.checkDependencies(of: packageLib2, are: [])
+            }
             results.delegate.checkNoDiagnostics()
         }
     }

From 47030cc0f1136284c11544cd060d0cd07494d1ce Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 15 Jul 2025 08:39:57 -0700
Subject: [PATCH 17/49] Make Android NDK discovery more robust, and add tests

Add support for Android NDK versions 23 through 26, and emit an explicit error for older versions. Add test coverage to verify parsing of the metadata.

I chose 23 as the cutoff for now simply because that's the next last time that the abis.json schema changed. That version was released in  August 2021. We can add older versions if anyone really wants them.
---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   | 231 +++++++----
 Sources/SWBAndroidPlatform/Plugin.swift       |  11 +-
 .../AndroidSDKTests.swift                     | 361 ++++++++++++++++++
 3 files changed, 523 insertions(+), 80 deletions(-)
 create mode 100644 Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift

diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index 44b93114..cb66619d 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -10,30 +10,51 @@
 //
 //===----------------------------------------------------------------------===//
 
-import SWBUtil
-import Foundation
+public import SWBUtil
+public import Foundation
 
-struct AndroidSDK: Sendable {
+@_spi(Testing) public struct AndroidSDK: Sendable {
     public let host: OperatingSystem
     public let path: Path
-    public let ndkVersion: Version?
+
+    /// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
+    @_spi(Testing) public let ndks: [NDK]
+
+    public var latestNDK: NDK? {
+        ndks.last
+    }
 
     init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
         self.host = host
         self.path = path
+        self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
+    }
 
-        let ndkBasePath = path.join("ndk")
-        if fs.exists(ndkBasePath) {
-            self.ndkVersion = try fs.listdir(ndkBasePath).map { try Version($0) }.max()
-        } else {
-            self.ndkVersion = nil
-        }
+    @_spi(Testing) public struct NDK: Equatable, Sendable {
+        public static let minimumNDKVersion = Version(23)
+
+        public let host: OperatingSystem
+        public let path: Path
+        public let version: Version
+        public let abis: [String: ABI]
+        public let deploymentTargetRange: DeploymentTargetRange
+
+        init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
+            self.host = host
+            self.path = ndkPath
+            self.version = version
 
-        if let ndkVersion {
-            let ndkPath = ndkBasePath.join(ndkVersion.description)
             let metaPath = ndkPath.join("meta")
 
-            self.abis = try JSONDecoder().decode([String: ABI].self, from: Data(fs.read(metaPath.join("abis.json"))))
+            guard #available(macOS 14, *) else {
+                throw StubError.error("Unsupported macOS version")
+            }
+
+            if version < Self.minimumNDKVersion {
+                throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
+            }
+
+            self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis
 
             struct PlatformsInfo: Codable {
                 let min: Int
@@ -41,87 +62,145 @@ struct AndroidSDK: Sendable {
             }
 
             let platformsInfo = try JSONDecoder().decode(PlatformsInfo.self, from: Data(fs.read(metaPath.join("platforms.json"))))
-            self.ndkPath = ndkPath
-            deploymentTargetRange = (platformsInfo.min, platformsInfo.max)
-        } else {
-            ndkPath = nil
-            deploymentTargetRange = nil
-            abis = nil
+            deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
         }
-    }
 
-    struct ABI: Codable {
-        enum Bitness: Int, Codable {
-            case bits32 = 32
-            case bits64 = 64
+        struct ABIs: DecodableWithConfiguration {
+            let abis: [String: ABI]
+
+            init(from decoder: any Decoder, configuration: Version) throws {
+                struct DynamicCodingKey: CodingKey {
+                    var stringValue: String
+                    
+                    init?(stringValue: String) {
+                        self.stringValue = stringValue
+                    }
+                    
+                    let intValue: Int? = nil
+
+                    init?(intValue: Int) {
+                        nil
+                    }
+                }
+                let container = try decoder.container(keyedBy: DynamicCodingKey.self)
+                abis = try Dictionary(uniqueKeysWithValues: container.allKeys.map { try ($0.stringValue, container.decode(ABI.self, forKey: $0, configuration: configuration)) })
+            }
         }
 
-        struct LLVMTriple: Codable {
-            var arch: String
-            var vendor: String
-            var system: String
-            var environment: String
-
-            var description: String {
-                "\(arch)-\(vendor)-\(system)-\(environment)"
+        @_spi(Testing) public struct ABI: DecodableWithConfiguration, Equatable, Sendable {
+            @_spi(Testing) public enum Bitness: Int, Codable, Equatable, Sendable {
+                case bits32 = 32
+                case bits64 = 64
             }
 
-            init(from decoder: any Decoder) throws {
-                let container = try decoder.singleValueContainer()
-                let triple = try container.decode(String.self)
-                if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
-                    self.arch = String(match.output.arch)
-                    self.vendor = String(match.output.vendor)
-                    self.system = String(match.output.system)
-                    self.environment = String(match.output.environment)
-                } else {
-                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
+            @_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable {
+                public var arch: String
+                public var vendor: String
+                public var system: String
+                public var environment: String
+
+                var description: String {
+                    "\(arch)-\(vendor)-\(system)-\(environment)"
+                }
+
+                public init(from decoder: any Decoder) throws {
+                    let container = try decoder.singleValueContainer()
+                    let triple = try container.decode(String.self)
+                    if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
+                        self.arch = String(match.output.arch)
+                        self.vendor = String(match.output.vendor)
+                        self.system = String(match.output.system)
+                        self.environment = String(match.output.environment)
+                    } else {
+                        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
+                    }
                 }
             }
 
-            func encode(to encoder: any Encoder) throws {
-                var container = encoder.singleValueContainer()
-                try container.encode(description)
+            public let bitness: Bitness
+            public let `default`: Bool
+            public let deprecated: Bool
+            public let proc: String
+            public let arch: String
+            public let triple: String
+            public let llvm_triple: LLVMTriple
+            public let min_os_version: Int
+
+            enum CodingKeys: String, CodingKey {
+                case bitness
+                case `default` = "default"
+                case deprecated
+                case proc
+                case arch
+                case triple
+                case llvm_triple = "llvm_triple"
+                case min_os_version = "min_os_version"
+            }
+
+            public init(from decoder: any Decoder, configuration ndkVersion: Version) throws {
+                let container = try decoder.container(keyedBy: CodingKeys.self)
+                self.bitness = try container.decode(Bitness.self, forKey: .bitness)
+                self.default = try container.decode(Bool.self, forKey: .default)
+                self.deprecated = try container.decode(Bool.self, forKey: .deprecated)
+                self.proc = try container.decode(String.self, forKey: .proc)
+                self.arch = try container.decode(String.self, forKey: .arch)
+                self.triple = try container.decode(String.self, forKey: .triple)
+                self.llvm_triple = try container.decode(LLVMTriple.self, forKey: .llvm_triple)
+                self.min_os_version = try container.decodeIfPresent(Int.self, forKey: .min_os_version) ?? {
+                    if ndkVersion < Version(27) {
+                        return 21 // min_os_version wasn't present prior to NDKr27, fill it in with 21, which is the appropriate value
+                    } else {
+                        throw DecodingError.valueNotFound(Int.self, .init(codingPath: container.codingPath, debugDescription: "No value associated with key \(CodingKeys.min_os_version) (\"\(CodingKeys.min_os_version.rawValue)\")."))
+                    }
+                }()
             }
         }
 
-        let bitness: Bitness
-        let `default`: Bool
-        let deprecated: Bool
-        let proc: String
-        let arch: String
-        let triple: String
-        let llvm_triple: LLVMTriple
-        let min_os_version: Int
-    }
+        @_spi(Testing) public struct DeploymentTargetRange: Equatable, Sendable {
+            public let min: Int
+            public let max: Int
+        }
 
-    public let abis: [String: ABI]?
+        public var toolchainPath: Path {
+            path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
+        }
 
-    public let deploymentTargetRange: (min: Int, max: Int)?
+        public var sysroot: Path {
+            toolchainPath.join("sysroot")
+        }
 
-    public let ndkPath: Path?
+        private var hostTag: String? {
+            switch host {
+            case .windows:
+                // Also works on Windows on ARM via Prism binary translation.
+                "windows-x86_64"
+            case .macOS:
+                // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
+                "darwin-x86_64"
+            case .linux:
+                // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
+                "linux-x86_64"
+            default:
+                nil // unsupported host
+            }
+        }
 
-    public var toolchainPath: Path? {
-        ndkPath?.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
-    }
+        public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
+            let ndkBasePath = sdkPath.join("ndk")
+            guard fs.exists(ndkBasePath) else {
+                return []
+            }
 
-    public var sysroot: Path? {
-        toolchainPath?.join("sysroot")
-    }
+            let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
+            let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }
 
-    private var hostTag: String? {
-        switch host {
-        case .windows:
-            // Also works on Windows on ARM via Prism binary translation.
-            "windows-x86_64"
-        case .macOS:
-            // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
-            "darwin-x86_64"
-        case .linux:
-            // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
-            "linux-x86_64"
-        default:
-            nil // unsupported host
+            // If we have some NDKs but all of them are unsupported, try parsing them so that parsing fails and provides a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
+            let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks
+
+            return try discoveredNdks.map { ndkVersion in
+                let ndkPath = ndkBasePath.join(ndkVersion.description)
+                return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
+            }
         }
     }
 
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 8b6c8d5a..88fa5f76 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -54,7 +54,7 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
             if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
                 return [
                     "ANDROID_SDK_ROOT": latest.path.str,
-                    "ANDROID_NDK_ROOT": latest.ndkPath?.str,
+                    "ANDROID_NDK_ROOT": latest.ndks.last?.path.str,
                 ].compactMapValues { $0 }
             }
         default:
@@ -112,10 +112,13 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             return []
         }
 
-        guard let abis = androidSdk.abis, let deploymentTargetRange = androidSdk.deploymentTargetRange else {
+        guard let androidNdk = androidSdk.latestNDK else {
             return []
         }
 
+        let abis = androidNdk.abis
+        let deploymentTargetRange = androidNdk.deploymentTargetRange
+
         let allPossibleTriples = abis.values.flatMap { abi in
             (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
                 var triple = abi.llvm_triple
@@ -147,7 +150,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             swiftSettings = [:]
         }
 
-        return [(androidSdk.sysroot ?? .root, androidPlatform, [
+        return [(androidNdk.sysroot, androidPlatform, [
             "Type": .plString("SDK"),
             "Version": .plString("0.0.0"),
             "CanonicalName": .plString("android"),
@@ -184,7 +187,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
     let plugin: AndroidPlugin
 
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
-        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.toolchainPath else {
+        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else {
             return []
         }
 
diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
new file mode 100644
index 00000000..37329d94
--- /dev/null
+++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
@@ -0,0 +1,361 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+@_spi(Testing) import SWBAndroidPlatform
+import SWBTestSupport
+import SWBUtil
+import Testing
+
+@Suite
+fileprivate struct AndroidSDKTests {
+    @Test func findInstallations() async throws {
+        let host = try ProcessInfo.processInfo.hostOperatingSystem()
+        let installations = try await AndroidSDK.findInstallations(host: host, fs: localFS)
+        // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed
+        for installation in installations {
+            #expect(installation.host == host)
+            #expect(installation.latestNDK == installation.ndks.last)
+        }
+    }
+
+    @Test func abis_r22() async throws {
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in
+            let error = #expect(throws: StubError.self) {
+                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            }
+            #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)")
+        }
+    }
+
+    @Test func abis_r26_3() async throws {
+        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+                contents <<<
+                """
+                {
+                    "armeabi-v7a": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "armv7-a",
+                        "arch": "arm",
+                        "triple": "arm-linux-androideabi",
+                        "llvm_triple": "armv7-none-linux-androideabi"
+                    },
+                    "arm64-v8a": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "aarch64",
+                        "arch": "arm64",
+                        "triple": "aarch64-linux-android",
+                        "llvm_triple": "aarch64-none-linux-android"
+                    },
+                    "x86": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "i686",
+                        "arch": "x86",
+                        "triple": "i686-linux-android",
+                        "llvm_triple": "i686-none-linux-android"
+                    },
+                    "x86_64": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "x86_64",
+                        "arch": "x86_64",
+                        "triple": "x86_64-linux-android",
+                        "llvm_triple": "x86_64-none-linux-android"
+                    }
+                }
+                """
+            }
+
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+                contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 34,
+                    "aliases": {
+                        "20": 19,
+                        "25": 24,
+                        "J": 16,
+                        "J-MR1": 17,
+                        "J-MR2": 18,
+                        "K": 19,
+                        "L": 21,
+                        "L-MR1": 22,
+                        "M": 23,
+                        "N": 24,
+                        "N-MR1": 24,
+                        "O": 26,
+                        "O-MR1": 27,
+                        "P": 28,
+                        "Q": 29,
+                        "R": 30,
+                        "S": 31,
+                        "Sv2": 32,
+                        "Tiramisu": 33,
+                        "UpsideDownCake": 34
+                    }
+                }
+                """
+            }
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            let installation = try #require(installations.only)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPath)
+            #expect(try installation.version == Version("26.3.11579264"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 34)
+
+            #expect(installation.abis.count == 4)
+
+            let armv7 = try #require(installation.abis["armeabi-v7a"])
+            #expect(armv7.bitness == .bits32)
+            #expect(armv7.default == true)
+            #expect(armv7.deprecated == false)
+            #expect(armv7.proc == "armv7-a")
+            #expect(armv7.arch == "arm")
+            #expect(armv7.triple == "arm-linux-androideabi")
+            #expect(armv7.llvm_triple.arch == "armv7")
+            #expect(armv7.llvm_triple.vendor == "none")
+            #expect(armv7.llvm_triple.system == "linux")
+            #expect(armv7.llvm_triple.environment == "androideabi")
+            #expect(armv7.min_os_version == 21)
+
+            let arm64 = try #require(installation.abis["arm64-v8a"])
+            #expect(arm64.bitness == .bits64)
+            #expect(arm64.default == true)
+            #expect(arm64.deprecated == false)
+            #expect(arm64.proc == "aarch64")
+            #expect(arm64.arch == "arm64")
+            #expect(arm64.triple == "aarch64-linux-android")
+            #expect(arm64.llvm_triple.arch == "aarch64")
+            #expect(arm64.llvm_triple.vendor == "none")
+            #expect(arm64.llvm_triple.system == "linux")
+            #expect(arm64.llvm_triple.environment == "android")
+            #expect(arm64.min_os_version == 21)
+
+            let x86 = try #require(installation.abis["x86"])
+            #expect(x86.bitness == .bits32)
+            #expect(x86.default == true)
+            #expect(x86.deprecated == false)
+            #expect(x86.proc == "i686")
+            #expect(x86.arch == "x86")
+            #expect(x86.triple == "i686-linux-android")
+            #expect(x86.llvm_triple.arch == "i686")
+            #expect(x86.llvm_triple.vendor == "none")
+            #expect(x86.llvm_triple.system == "linux")
+            #expect(x86.llvm_triple.environment == "android")
+            #expect(x86.min_os_version == 21)
+
+            let x86_64 = try #require(installation.abis["x86_64"])
+            #expect(x86_64.bitness == .bits64)
+            #expect(x86_64.default == true)
+            #expect(x86_64.deprecated == false)
+            #expect(x86_64.proc == "x86_64")
+            #expect(x86_64.arch == "x86_64")
+            #expect(x86_64.triple == "x86_64-linux-android")
+            #expect(x86_64.llvm_triple.arch == "x86_64")
+            #expect(x86_64.llvm_triple.vendor == "none")
+            #expect(x86_64.llvm_triple.system == "linux")
+            #expect(x86_64.llvm_triple.environment == "android")
+            #expect(x86_64.min_os_version == 21)
+        }
+    }
+
+    @Test func abis_r27() async throws {
+        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+                contents <<<
+                """
+                {
+                    "armeabi-v7a": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "armv7-a",
+                        "arch": "arm",
+                        "triple": "arm-linux-androideabi",
+                        "llvm_triple": "armv7-none-linux-androideabi",
+                        "min_os_version": 21
+                    },
+                    "arm64-v8a": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "aarch64",
+                        "arch": "arm64",
+                        "triple": "aarch64-linux-android",
+                        "llvm_triple": "aarch64-none-linux-android",
+                        "min_os_version": 21
+                    },
+                    "riscv64": {
+                        "bitness": 64,
+                        "default": false,
+                        "deprecated": false,
+                        "proc": "riscv64",
+                        "arch": "riscv64",
+                        "triple": "riscv64-linux-android",
+                        "llvm_triple": "riscv64-none-linux-android",
+                        "min_os_version": 35
+                    },
+                    "x86": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "i686",
+                        "arch": "x86",
+                        "triple": "i686-linux-android",
+                        "llvm_triple": "i686-none-linux-android",
+                        "min_os_version": 21
+                    },
+                    "x86_64": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "x86_64",
+                        "arch": "x86_64",
+                        "triple": "x86_64-linux-android",
+                        "llvm_triple": "x86_64-none-linux-android",
+                        "min_os_version": 21
+                    }
+                }
+                """
+            }
+
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+                contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 35,
+                    "aliases": {
+                        "20": 19,
+                        "25": 24,
+                        "J": 16,
+                        "J-MR1": 17,
+                        "J-MR2": 18,
+                        "K": 19,
+                        "L": 21,
+                        "L-MR1": 22,
+                        "M": 23,
+                        "N": 24,
+                        "N-MR1": 24,
+                        "O": 26,
+                        "O-MR1": 27,
+                        "P": 28,
+                        "Q": 29,
+                        "R": 30,
+                        "S": 31,
+                        "Sv2": 32,
+                        "Tiramisu": 33,
+                        "UpsideDownCake": 34,
+                        "VanillaIceCream": 35
+                    }
+                }
+                """
+            }
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            let installation = try #require(installations.only)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPath)
+            #expect(try installation.version == Version("27.0.11718014"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 35)
+
+            #expect(installation.abis.count == 5)
+
+            let armv7 = try #require(installation.abis["armeabi-v7a"])
+            #expect(armv7.bitness == .bits32)
+            #expect(armv7.default == true)
+            #expect(armv7.deprecated == false)
+            #expect(armv7.proc == "armv7-a")
+            #expect(armv7.arch == "arm")
+            #expect(armv7.triple == "arm-linux-androideabi")
+            #expect(armv7.llvm_triple.arch == "armv7")
+            #expect(armv7.llvm_triple.vendor == "none")
+            #expect(armv7.llvm_triple.system == "linux")
+            #expect(armv7.llvm_triple.environment == "androideabi")
+            #expect(armv7.min_os_version == 21)
+
+            let arm64 = try #require(installation.abis["arm64-v8a"])
+            #expect(arm64.bitness == .bits64)
+            #expect(arm64.default == true)
+            #expect(arm64.deprecated == false)
+            #expect(arm64.proc == "aarch64")
+            #expect(arm64.arch == "arm64")
+            #expect(arm64.triple == "aarch64-linux-android")
+            #expect(arm64.llvm_triple.arch == "aarch64")
+            #expect(arm64.llvm_triple.vendor == "none")
+            #expect(arm64.llvm_triple.system == "linux")
+            #expect(arm64.llvm_triple.environment == "android")
+            #expect(arm64.min_os_version == 21)
+
+            let riscv64 = try #require(installation.abis["riscv64"])
+            #expect(riscv64.bitness == .bits64)
+            #expect(riscv64.default == false)
+            #expect(riscv64.deprecated == false)
+            #expect(riscv64.proc == "riscv64")
+            #expect(riscv64.arch == "riscv64")
+            #expect(riscv64.triple == "riscv64-linux-android")
+            #expect(riscv64.llvm_triple.arch == "riscv64")
+            #expect(riscv64.llvm_triple.vendor == "none")
+            #expect(riscv64.llvm_triple.system == "linux")
+            #expect(riscv64.llvm_triple.environment == "android")
+            #expect(riscv64.min_os_version == 35)
+
+            let x86 = try #require(installation.abis["x86"])
+            #expect(x86.bitness == .bits32)
+            #expect(x86.default == true)
+            #expect(x86.deprecated == false)
+            #expect(x86.proc == "i686")
+            #expect(x86.arch == "x86")
+            #expect(x86.triple == "i686-linux-android")
+            #expect(x86.llvm_triple.arch == "i686")
+            #expect(x86.llvm_triple.vendor == "none")
+            #expect(x86.llvm_triple.system == "linux")
+            #expect(x86.llvm_triple.environment == "android")
+            #expect(x86.min_os_version == 21)
+
+            let x86_64 = try #require(installation.abis["x86_64"])
+            #expect(x86_64.bitness == .bits64)
+            #expect(x86_64.default == true)
+            #expect(x86_64.deprecated == false)
+            #expect(x86_64.proc == "x86_64")
+            #expect(x86_64.arch == "x86_64")
+            #expect(x86_64.triple == "x86_64-linux-android")
+            #expect(x86_64.llvm_triple.arch == "x86_64")
+            #expect(x86_64.llvm_triple.vendor == "none")
+            #expect(x86_64.llvm_triple.system == "linux")
+            #expect(x86_64.llvm_triple.environment == "android")
+            #expect(x86_64.min_os_version == 21)
+        }
+    }
+
+    private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws {
+        let fs = PseudoFS()
+        let ndkPath = Path.root.join("ndk")
+        let ndkVersionPath = ndkPath.join(version.description)
+        try fs.createDirectory(ndkPath, recursive: true)
+        try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+        let host = try ProcessInfo.processInfo.hostOperatingSystem()
+        try await block(host, fs, ndkVersionPath)
+    }
+}

From 70af35a0b7bf13d50f57a0dcf9807340db0c74bb Mon Sep 17 00:00:00 2001
From: baujla <baujla@apple.com>
Date: Tue, 29 Jul 2025 16:14:50 -0700
Subject: [PATCH 18/49] Pr/update usage description keys (#682)

* Adds build settings for missing usage description strings

* Fixes NSCalendarsWriteOnlyAccessUsageDescription
---
 Sources/SWBCore/Settings/BuiltinMacros.swift  | 20 ++++++
 Sources/SWBCore/Specs/CoreBuildSystem.xcspec  | 70 +++++++++++++++++++
 .../InfoPlistProcessorTaskAction.swift        |  9 +++
 3 files changed, 99 insertions(+)

diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift
index 04bd1c66..bef7fc81 100644
--- a/Sources/SWBCore/Settings/BuiltinMacros.swift
+++ b/Sources/SWBCore/Settings/BuiltinMacros.swift
@@ -1215,14 +1215,19 @@ public final class BuiltinMacros {
     public static let INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = BuiltinMacros.declareBooleanMacro("INFOPLIST_KEY_ITSAppUsesNonExemptEncryption")
     public static let INFOPLIST_KEY_ITSEncryptionExportComplianceCode = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_ITSEncryptionExportComplianceCode")
     public static let INFOPLIST_KEY_NFCReaderUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NFCReaderUsageDescription")
+    public static let INFOPLIST_KEY_NSAccessoryTrackingUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSAccessoryTrackingUsageDescription")
+    public static let INFOPLIST_KEY_NSAppDataUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSAppDataUsageDescription")
     public static let INFOPLIST_KEY_NSAppleEventsUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSAppleEventsUsageDescription")
     public static let INFOPLIST_KEY_NSAppleMusicUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSAppleMusicUsageDescription")
     public static let INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription")
     public static let INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription")
     public static let INFOPLIST_KEY_NSBluetoothWhileInUseUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSBluetoothWhileInUseUsageDescription")
     public static let INFOPLIST_KEY_NSCalendarsUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSCalendarsUsageDescription")
+    public static let INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription")
+    public static let INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription")
     public static let INFOPLIST_KEY_NSCameraUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSCameraUsageDescription")
     public static let INFOPLIST_KEY_NSContactsUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSContactsUsageDescription")
+    public static let INFOPLIST_KEY_NSCriticalMessagingUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSCriticalMessagingUsageDescription")
     public static let INFOPLIST_KEY_NSDesktopFolderUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSDesktopFolderUsageDescription")
     public static let INFOPLIST_KEY_NSDocumentsFolderUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSDocumentsFolderUsageDescription")
     public static let INFOPLIST_KEY_NSDownloadsFolderUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSDownloadsFolderUsageDescription")
@@ -1230,8 +1235,10 @@ public final class BuiltinMacros {
     public static let INFOPLIST_KEY_NSFallDetectionUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSFallDetectionUsageDescription")
     public static let INFOPLIST_KEY_NSFileProviderDomainUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSFileProviderDomainUsageDescription")
     public static let INFOPLIST_KEY_NSFileProviderPresenceUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSFileProviderPresenceUsageDescription")
+    public static let INFOPLIST_KEY_NSFinancialDataUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSFinancialDataUsageDescription")
     public static let INFOPLIST_KEY_NSFocusStatusUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSFocusStatusUsageDescription")
     public static let INFOPLIST_KEY_NSGKFriendListUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSGKFriendListUsageDescription")
+    public static let INFOPLIST_KEY_NSHandsTrackingUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSHandsTrackingUsageDescription")
     public static let INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription")
     public static let INFOPLIST_KEY_NSHealthShareUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSHealthShareUsageDescription")
     public static let INFOPLIST_KEY_NSHealthUpdateUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSHealthUpdateUsageDescription")
@@ -1243,6 +1250,7 @@ public final class BuiltinMacros {
     public static let INFOPLIST_KEY_NSLocationTemporaryUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSLocationTemporaryUsageDescription")
     public static let INFOPLIST_KEY_NSLocationUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSLocationUsageDescription")
     public static let INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSLocationWhenInUseUsageDescription")
+    public static let INFOPLIST_KEY_NSMainCameraUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSMainCameraUsageDescription")
     public static let INFOPLIST_KEY_NSMicrophoneUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSMicrophoneUsageDescription")
     public static let INFOPLIST_KEY_NSMotionUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSMotionUsageDescription")
     public static let INFOPLIST_KEY_NSNearbyInteractionAllowOnceUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSNearbyInteractionAllowOnceUsageDescription")
@@ -1251,6 +1259,7 @@ public final class BuiltinMacros {
     public static let INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription")
     public static let INFOPLIST_KEY_NSPhotoLibraryUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSPhotoLibraryUsageDescription")
     public static let INFOPLIST_KEY_NSRemindersUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSRemindersUsageDescription")
+    public static let INFOPLIST_KEY_NSRemindersFullAccessUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSRemindersFullAccessUsageDescription")
     public static let INFOPLIST_KEY_NSRemovableVolumesUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSRemovableVolumesUsageDescription")
     public static let INFOPLIST_KEY_NSSensorKitPrivacyPolicyURL = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSSensorKitPrivacyPolicyURL")
     public static let INFOPLIST_KEY_NSSensorKitUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSSensorKitUsageDescription")
@@ -1261,6 +1270,7 @@ public final class BuiltinMacros {
     public static let INFOPLIST_KEY_NSUserTrackingUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSUserTrackingUsageDescription")
     public static let INFOPLIST_KEY_NSVideoSubscriberAccountUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSVideoSubscriberAccountUsageDescription")
     public static let INFOPLIST_KEY_NSVoIPUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSVoIPUsageDescription")
+    public static let INFOPLIST_KEY_NSWorldSensingUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_NSWorldSensingUsageDescription")
     public static let INFOPLIST_KEY_OSBundleUsageDescription = BuiltinMacros.declareStringMacro("INFOPLIST_KEY_OSBundleUsageDescription")
 
     // Info.plist Keys - macOS
@@ -2454,6 +2464,8 @@ public final class BuiltinMacros {
         // Info.plist Keys - Usage Descriptions
         INFOPLIST_KEY_ITSAppUsesNonExemptEncryption,
         INFOPLIST_KEY_ITSEncryptionExportComplianceCode,
+        INFOPLIST_KEY_NSAccessoryTrackingUsageDescription,
+        INFOPLIST_KEY_NSAppDataUsageDescription,
         INFOPLIST_KEY_NFCReaderUsageDescription,
         INFOPLIST_KEY_NSAppleEventsUsageDescription,
         INFOPLIST_KEY_NSAppleMusicUsageDescription,
@@ -2461,8 +2473,11 @@ public final class BuiltinMacros {
         INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription,
         INFOPLIST_KEY_NSBluetoothWhileInUseUsageDescription,
         INFOPLIST_KEY_NSCalendarsUsageDescription,
+        INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription,
+        INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription,
         INFOPLIST_KEY_NSCameraUsageDescription,
         INFOPLIST_KEY_NSContactsUsageDescription,
+        INFOPLIST_KEY_NSCriticalMessagingUsageDescription,
         INFOPLIST_KEY_NSDesktopFolderUsageDescription,
         INFOPLIST_KEY_NSDocumentsFolderUsageDescription,
         INFOPLIST_KEY_NSDownloadsFolderUsageDescription,
@@ -2470,8 +2485,10 @@ public final class BuiltinMacros {
         INFOPLIST_KEY_NSFallDetectionUsageDescription,
         INFOPLIST_KEY_NSFileProviderDomainUsageDescription,
         INFOPLIST_KEY_NSFileProviderPresenceUsageDescription,
+        INFOPLIST_KEY_NSFinancialDataUsageDescription,
         INFOPLIST_KEY_NSFocusStatusUsageDescription,
         INFOPLIST_KEY_NSGKFriendListUsageDescription,
+        INFOPLIST_KEY_NSHandsTrackingUsageDescription,
         INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription,
         INFOPLIST_KEY_NSHealthShareUsageDescription,
         INFOPLIST_KEY_NSHealthUpdateUsageDescription,
@@ -2483,6 +2500,7 @@ public final class BuiltinMacros {
         INFOPLIST_KEY_NSLocationTemporaryUsageDescription,
         INFOPLIST_KEY_NSLocationUsageDescription,
         INFOPLIST_KEY_NSLocationWhenInUseUsageDescription,
+        INFOPLIST_KEY_NSMainCameraUsageDescription,
         INFOPLIST_KEY_NSMicrophoneUsageDescription,
         INFOPLIST_KEY_NSMotionUsageDescription,
         INFOPLIST_KEY_NSNearbyInteractionAllowOnceUsageDescription,
@@ -2491,6 +2509,7 @@ public final class BuiltinMacros {
         INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription,
         INFOPLIST_KEY_NSPhotoLibraryUsageDescription,
         INFOPLIST_KEY_NSRemindersUsageDescription,
+        INFOPLIST_KEY_NSRemindersFullAccessUsageDescription,
         INFOPLIST_KEY_NSRemovableVolumesUsageDescription,
         INFOPLIST_KEY_NSSensorKitPrivacyPolicyURL,
         INFOPLIST_KEY_NSSensorKitUsageDescription,
@@ -2501,6 +2520,7 @@ public final class BuiltinMacros {
         INFOPLIST_KEY_NSUserTrackingUsageDescription,
         INFOPLIST_KEY_NSVideoSubscriberAccountUsageDescription,
         INFOPLIST_KEY_NSVoIPUsageDescription,
+        INFOPLIST_KEY_NSWorldSensingUsageDescription,
         INFOPLIST_KEY_OSBundleUsageDescription,
 
         // Info.plist Keys - macOS
diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
index b1ad1195..f71e8276 100644
--- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
+++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
@@ -3902,6 +3902,20 @@ When this setting is enabled:
                 DisplayName = "Privacy - NFC Scan Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NFCReaderUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nfcreaderusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSAccessoryTrackingUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Accessory Tracking Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSAccessoryTrackingUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsaccessorytrackingusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
+            {
+                Name = INFOPLIST_KEY_NSAppDataUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Other Application Data Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSAppDataUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsappdatausagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSAppleEventsUsageDescription;
                 Type = String;
@@ -3944,6 +3958,20 @@ When this setting is enabled:
                 DisplayName = "Privacy - Calendars Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSCalendarsUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscalendarsusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSCalendarsFullAccessUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Calendars Full Access Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSCalendarsFullAccessUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscalendarsfullaccessusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
+            {
+                Name = INFOPLIST_KEY_NSCalendarsWriteOnlyAccessUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Calendars Write Only Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSCalendarsWriteOnlyAccessUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscalendarswriteonlyaccessusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSCameraUsageDescription;
                 Type = String;
@@ -3958,6 +3986,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - Contacts Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSContactsUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscontactsusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSCriticalMessagingUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Critical Messaging Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSCriticalMessagingUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nscriticalmessagingusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSDesktopFolderUsageDescription;
                 Type = String;
@@ -4007,6 +4042,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - File Provider Presence Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the NSFileProviderPresenceUsageDescription key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSFinancialDataUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Financial Data Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSFinancialDataUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsfinancialdatausagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSFocusStatusUsageDescription;
                 Type = String;
@@ -4021,6 +4063,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - GameKit Friend List Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSGKFriendListUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsgkfriendlistusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSHandsTrackingUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Hands Tracking Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSHandsTrackingUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nshandstrackingusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSHealthClinicalHealthRecordsShareUsageDescription;
                 Type = String;
@@ -4098,6 +4147,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - Location When In Use Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSLocationWhenInUseUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationwheninuseusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSMainCameraUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Main Camera Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSMainCameraUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsmaincamerausagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSMicrophoneUsageDescription;
                 Type = String;
@@ -4154,6 +4210,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - Reminders Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSRemindersUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsremindersusagedescription) key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSRemindersFullAccessUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - Reminders Full Access Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSRemindersFullAccessUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsremindersfullaccessusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_NSRemovableVolumesUsageDescription;
                 Type = String;
@@ -4224,6 +4287,13 @@ When this setting is enabled:
                 DisplayName = "Privacy - VoIP Usage Description";
                 Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the NSVoIPUsageDescription key in the `Info.plist` file to the value of this build setting.";
             },
+            {
+                Name = INFOPLIST_KEY_NSWorldSensingUsageDescription;
+                Type = String;
+                Category = "Info.plist Values";
+                DisplayName = "Privacy - World Sensing Usage Description";
+                Description = "When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [NSWorldSensingUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nsworldsensingusagedescription) key in the `Info.plist` file to the value of this build setting.";
+            },
             {
                 Name = INFOPLIST_KEY_OSBundleUsageDescription;
                 Type = String;
diff --git a/Sources/SWBTaskExecution/TaskActions/InfoPlistProcessorTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/InfoPlistProcessorTaskAction.swift
index e7ad3aeb..225cc2c8 100644
--- a/Sources/SWBTaskExecution/TaskActions/InfoPlistProcessorTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/InfoPlistProcessorTaskAction.swift
@@ -634,14 +634,19 @@ public final class InfoPlistProcessorTaskAction: TaskAction
 
     private let usageDescriptionStringKeys: Set<String> = [
         "NFCReaderUsageDescription",
+        "NSAccessoryTrackingUsageDescription",
+        "NSAppDataUsageDescription",
         "NSAppleEventsUsageDescription",
         "NSAppleMusicUsageDescription",
         "NSBluetoothAlwaysUsageDescription",
         "NSBluetoothPeripheralUsageDescription",
         "NSBluetoothWhileInUseUsageDescription",
         "NSCalendarsUsageDescription",
+        "NSCalendarsFullAccessUsageDescription",
+        "NSCalendarsWriteOnlyAccessUsageDescription",
         "NSCameraUsageDescription",
         "NSContactsUsageDescription",
+        "NSCriticalMessagingUsageDescription",
         "NSDesktopFolderUsageDescription",
         "NSDocumentsFolderUsageDescription",
         "NSDownloadsFolderUsageDescription",
@@ -649,8 +654,10 @@ public final class InfoPlistProcessorTaskAction: TaskAction
         "NSFallDetectionUsageDescription",
         "NSFileProviderDomainUsageDescription",
         "NSFileProviderPresenceUsageDescription",
+        "NSFinancialDataUsageDescription",
         "NSFocusStatusUsageDescription",
         "NSGKFriendListUsageDescription",
+        "NSHandsTrackingUsageDescription",
         "NSHealthClinicalHealthRecordsShareUsageDescription",
         "NSHealthShareUsageDescription",
         "NSHealthUpdateUsageDescription",
@@ -669,6 +676,7 @@ public final class InfoPlistProcessorTaskAction: TaskAction
         "NSPhotoLibraryAddUsageDescription",
         "NSPhotoLibraryUsageDescription",
         "NSRemindersUsageDescription",
+        "NSRemindersFullAccessUsageDescription",
         "NSRemovableVolumesUsageDescription",
         "NSSensorKitUsageDescription",
         "NSSiriUsageDescription",
@@ -678,6 +686,7 @@ public final class InfoPlistProcessorTaskAction: TaskAction
         "NSUserTrackingUsageDescription",
         "NSVideoSubscriberAccountUsageDescription",
         "NSVoIPUsageDescription",
+        "NSWorldSensingUsageDescription",
         "OSBundleUsageDescription",
     ]
 

From edfb4b345e15fd140a4ab65d364c6aaca4510792 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 29 Jul 2025 16:14:50 -0700
Subject: [PATCH 19/49] Add a hostRuntimeEnvironment test helper function

This may fix the few tests which are failing due to missing DLLs on Windows in certain environments.
---
 .../RunDestinationTestSupport.swift           | 20 +++++++++++++
 .../BuildOperationTests.swift                 | 28 ++-----------------
 .../CustomTaskBuildOperationTests.swift       |  9 ++----
 3 files changed, 25 insertions(+), 32 deletions(-)

diff --git a/Sources/SWBTestSupport/RunDestinationTestSupport.swift b/Sources/SWBTestSupport/RunDestinationTestSupport.swift
index 9f6ae3bf..ecdff852 100644
--- a/Sources/SWBTestSupport/RunDestinationTestSupport.swift
+++ b/Sources/SWBTestSupport/RunDestinationTestSupport.swift
@@ -296,6 +296,26 @@ extension RunDestinationInfo {
             return .elf
         }
     }
+
+    /// An `Environment` object with `PATH` or `LD_LIBRARY_PATH` set appropriately pointing into the toolchain to be able to run a built Swift binary in tests.
+    ///
+    /// - note: On macOS, the OS provided Swift runtime is used, so `DYLD_LIBRARY_PATH` is never set for Mach-O destinations.
+    package func hostRuntimeEnvironment(_ core: Core, initialEnvironment: Environment = Environment()) -> Environment {
+        var environment = initialEnvironment
+        guard let toolchain = core.toolchainRegistry.defaultToolchain else {
+            return environment
+        }
+        switch imageFormat(core) {
+        case .elf:
+            environment.prependPath(key: "LD_LIBRARY_PATH", value: toolchain.path.join("usr/lib/swift/\(platform)").str)
+        case .pe:
+            environment.prependPath(key: .path, value: core.developerPath.path.join("Runtimes").join(toolchain.version.description).join("usr/bin").str)
+        case .macho:
+            // Fall back to the OS provided Swift runtime
+            break
+        }
+        return environment
+    }
 }
 
 extension _RunDestinationInfo {
diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
index 869029c6..4e10efc7 100644
--- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
@@ -144,15 +144,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
             try await tester.checkBuild(runDestination: destination, signableTargets: Set(provisioningInputs.keys), signableTargetInputs: provisioningInputs) { results in
                 results.checkNoErrors()
 
-                let toolchain = try #require(core.toolchainRegistry.defaultToolchain)
-                let environment: Environment
-                if destination.imageFormat(core) == .elf {
-                    environment = ["LD_LIBRARY_PATH": toolchain.path.join("usr/lib/swift/\(destination.platform)").str]
-                } else {
-                    environment = .init()
-                }
-
-                let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "tool")).str), arguments: [], environment: environment)
+                let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "tool")).str), arguments: [], environment: destination.hostRuntimeEnvironment(core))
                 #expect(executionResult.exitStatus == .exit(0))
                 if core.hostOperatingSystem == .windows {
                     #expect(String(decoding: executionResult.stdout, as: UTF8.self) == "Hello world\r\n")
@@ -377,15 +369,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
                     }
                 }
 
-                let toolchain = try #require(try await getCore().toolchainRegistry.defaultToolchain)
-                let environment: Environment
-                if destination.platform == "linux" {
-                    environment = ["LD_LIBRARY_PATH": toolchain.path.join("usr/lib/swift/linux").str]
-                } else {
-                    environment = .init()
-                }
-
-                let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "tool")).str), arguments: [], environment: environment)
+                let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "tool")).str), arguments: [], environment: destination.hostRuntimeEnvironment(core))
                 #expect(executionResult.exitStatus == .exit(0))
                 if core.hostOperatingSystem == .windows {
                     #expect(String(decoding: executionResult.stdout, as: UTF8.self) == "Hello world\r\n")
@@ -480,13 +464,7 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
             try await tester.checkBuild(runDestination: destination, persistent: true) { results in
                 results.checkNoErrors()
 
-                let toolchain = try #require(try await getCore().toolchainRegistry.defaultToolchain)
-                let environment: Environment
-                if destination.platform == "linux" {
-                    environment = ["LD_LIBRARY_PATH": toolchain.path.join("usr/lib/swift/linux").str]
-                } else {
-                    environment = .init()
-                }
+                let environment = destination.hostRuntimeEnvironment(core)
 
                 let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: projectDir.join("build").join("Debug\(destination.builtProductsDirSuffix)").join(core.hostOperatingSystem.imageFormat.executableName(basename: "test.xctest")).str), arguments: ["--testing-library", "swift-testing"], environment: environment)
                 #expect(String(decoding: executionResult.stderr, as: UTF8.self).contains("Test run started"))
diff --git a/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift b/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift
index f94d48ac..763d508a 100644
--- a/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/CustomTaskBuildOperationTests.swift
@@ -30,12 +30,7 @@ fileprivate struct CustomTaskBuildOperationTests: CoreBasedTests {
             let destination: RunDestinationInfo = .host
             let core = try await getCore()
             let toolchain = try #require(core.toolchainRegistry.defaultToolchain)
-            let environment: [String: String]
-            if  destination.imageFormat(core) == .elf {
-                environment = ["LD_LIBRARY_PATH": toolchain.path.join("usr/lib/swift/\(destination.platform)").str]
-            } else {
-                environment = ProcessInfo.processInfo.environment.filter { $0.key.uppercased() == "PATH" } // important to allow swift to be looked up in PATH on Windows/Linux
-            }
+            let environment = destination.hostRuntimeEnvironment(core)
 
             let testProject = TestProject(
                 "aProject",
@@ -69,7 +64,7 @@ fileprivate struct CustomTaskBuildOperationTests: CoreBasedTests {
                         customTasks: [
                             TestCustomTask(
                                 commandLine: ["$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/tool\(destination == .windows ? ".exe" : "")"],
-                                environment: environment,
+                                environment: .init(environment),
                                 workingDirectory: tmpDir.str,
                                 executionDescription: "My Custom Task",
                                 inputs: ["$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/tool\(destination == .windows ? ".exe" : "")"],

From a678e467b30d0894e9fe082c73be82947ca9b3b7 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 29 Jul 2025 14:41:18 -0700
Subject: [PATCH 20/49] Enable some skipped tests

Some of these were formerly skipped in GitHub actions, but are passing now. Likely the culprit was ServiceConsoleTests.serviceShutdown all along, which is fixed in #673
---
 Tests/SWBTaskExecutionTests/PBXCpTests.swift  | 27 ++++++++++++++-----
 .../ConsoleCommands/BuildCommandTests.swift   |  2 +-
 .../CreateXCFrameworkCommandTests.swift       |  2 +-
 .../GeneralCommandsTests.swift                |  2 +-
 .../ConsoleCommands/ServiceConsoleTests.swift |  2 +-
 .../SessionCommandsTests.swift                |  2 +-
 .../ConsoleCommands/XcodeCommandsTests.swift  |  2 +-
 7 files changed, 26 insertions(+), 13 deletions(-)

diff --git a/Tests/SWBTaskExecutionTests/PBXCpTests.swift b/Tests/SWBTaskExecutionTests/PBXCpTests.swift
index cfb37a75..069101ef 100644
--- a/Tests/SWBTaskExecutionTests/PBXCpTests.swift
+++ b/Tests/SWBTaskExecutionTests/PBXCpTests.swift
@@ -442,21 +442,32 @@ fileprivate struct PBXCpTests: CoreBasedTests {
     fileprivate let buffer0 = [UInt8](repeating: 0xAA, count: 1024 * 513)
     fileprivate let buffer1 = [UInt8](repeating: 0x55, count: 1024 * 513)
 
-    @Test(.skipHostOS(.windows, "LocalFS needs to use stat64 on windows...."),
-          .skipInGitHubActions("GitHub action runners do not have enough storage space for this test"))
+    @Test
     func largerFile() async throws {
-        try await withTemporaryDirectory { tmp in
+        let iterationCount = 4096
+        let buffers = buffer0 + buffer1
+        let requiredSpace = ByteCount(Int64(buffers.count * (iterationCount + 1) * 2 * 2))
+        let fs = localFS
+        try await withTemporaryDirectory(fs: fs) { tmp in
+            // If the available free disk space on this filesystem is less than twice the amount of data the test is expected to write (2 copies of the file times 2), skip it
+            if let freeSpace = try fs.getFreeDiskSpace(tmp), freeSpace < requiredSpace {
+                withKnownIssue {
+                    Issue.record("There is not enough free disk space to run this test (required: \(requiredSpace), free: \(freeSpace))")
+                    return
+                }
+            }
+
             // Test copying a large file.
             let src = tmp.join("src")
             let sName = src.join("file")
             let dst = tmp.join("dst")
             let dName = dst.join("file")
-            let fs = localFS
+
 
             try fs.createDirectory(src, recursive: true)
             var size = 0
             try await fs.write(sName) { fd in
-                for _ in 0...4096 {
+                for _ in 0...iterationCount {
                     size += try fd.writeAll(buffer0)
                     size += try fd.writeAll(buffer1)
                 }
@@ -470,8 +481,10 @@ fileprivate struct PBXCpTests: CoreBasedTests {
                     "copying src/...\n   copying file...\n    \(size) bytes\n"
                 ))
                 // Check permissions
-                let dstPerm = try fs.getFilePermissions(dName)
-                #expect(dstPerm == 0o644) // files are created with u+rw, g+wr, o+rw (and +x if src is executable) permissions and umask will adjust
+                if try ProcessInfo.processInfo.hostOperatingSystem() != .windows {
+                    let dstPerm = try fs.getFilePermissions(dName)
+                    #expect(dstPerm == 0o644) // files are created with u+rw, g+wr, o+rw (and +x if src is executable) permissions and umask will adjust
+                }
                 #expect(FileManager.default.contentsEqual(atPath: sName.str, andPath: dName.str))
             }
         }
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/BuildCommandTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/BuildCommandTests.swift
index 002298bd..7bb4e1f5 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/BuildCommandTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/BuildCommandTests.swift
@@ -17,7 +17,7 @@ import SWBUtil
 import Testing
 import SwiftBuildTestSupport
 
-@Suite(.skipHostOS(.windows), .skipInGitHubActions("failing in the GitHub actions runner environment"))
+@Suite(.skipHostOS(.windows))
 fileprivate struct BuildCommandTests {
     private let commandSequenceCodec: any CommandSequenceEncodable = LLVMStyleCommandCodec()
 
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/CreateXCFrameworkCommandTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/CreateXCFrameworkCommandTests.swift
index 5cd01bb7..ba17164f 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/CreateXCFrameworkCommandTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/CreateXCFrameworkCommandTests.swift
@@ -17,7 +17,7 @@ import SWBTestSupport
 
 // Note: The functionality of this class is heavily unit tested in `XCFrameworkTests.swift`. These tests are only to ensure that the command is indeed hooked up and registered properly.
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+@Suite(.skipHostOS(.windows))
 fileprivate struct CreateXCFrameworkCommandTests {
     @Test
     func commandInvocation() async throws {
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/GeneralCommandsTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/GeneralCommandsTests.swift
index 0fdc8694..fe762297 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/GeneralCommandsTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/GeneralCommandsTests.swift
@@ -15,7 +15,7 @@ import SwiftBuild
 import SWBTestSupport
 import SWBUtil
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+@Suite(.skipHostOS(.windows))
 fileprivate struct GeneralCommandsTests {
     @Test(.skipHostOS(.windows), // PTY not supported on Windows
         .requireHostOS(.macOS)) // something with terminal echo is different on macOS vs Linux
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
index 9034ab94..467977df 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/ServiceConsoleTests.swift
@@ -25,7 +25,7 @@ import System
 import SystemPackage
 #endif
 
-@Suite(.skipHostOS(.windows))
+@Suite
 fileprivate struct ServiceConsoleTests {
     @Test
     func emptyInput() async throws {
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/SessionCommandsTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/SessionCommandsTests.swift
index 327e7d38..73dc40f4 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/SessionCommandsTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/SessionCommandsTests.swift
@@ -14,7 +14,7 @@ import SWBTestSupport
 import Testing
 import SWBUtil
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+@Suite(.skipHostOS(.windows))
 fileprivate struct SessionCommandsTests {
     @Test(.skipHostOS(.windows), // PTY not supported on Windows
         .requireHostOS(.macOS)) // something with terminal echo is different on macOS vs Linux
diff --git a/Tests/SwiftBuildTests/ConsoleCommands/XcodeCommandsTests.swift b/Tests/SwiftBuildTests/ConsoleCommands/XcodeCommandsTests.swift
index 6aaf3805..c481896e 100644
--- a/Tests/SwiftBuildTests/ConsoleCommands/XcodeCommandsTests.swift
+++ b/Tests/SwiftBuildTests/ConsoleCommands/XcodeCommandsTests.swift
@@ -14,7 +14,7 @@ import SWBTestSupport
 import Testing
 import SWBUtil
 
-@Suite(.skipInGitHubActions("failing in the GitHub actions runner environment"), .skipHostOS(.windows))
+@Suite(.skipHostOS(.windows))
 fileprivate struct XcodeCommandsTests {
     @Test(.skipHostOS(.windows), // PTY not supported on Windows
         .requireHostOS(.macOS)) // something with terminal echo is different on macOS vs Linux

From 7b0854e670460bcc3bef42174f29f2e022c2626f Mon Sep 17 00:00:00 2001
From: Mirza Garibovic <garibovic@apple.com>
Date: Thu, 10 Jul 2025 14:20:45 -0700
Subject: [PATCH 21/49] Dependencies: include moduleDependenciesContext in the
 Swift compilation requirements signature

---
 .../Tools/SwiftCompiler.swift                 | 19 ++++++++++++++++++-
 1 file changed, 18 insertions(+), 1 deletion(-)

diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
index bfd6b408..0bfa6c0b 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
@@ -2381,8 +2381,25 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                     dependencyData = nil
                 }
 
+                let compilationRequirementAdditionalSignatureData: String
+                if let moduleDependenciesContext = cbc.producer.moduleDependenciesContext {
+                    do {
+                        let jsonData = try JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(moduleDependenciesContext)
+                        guard let signature = String(data: jsonData, encoding: .utf8) else {
+                            throw StubError.error("non-UTF-8 data")
+                        }
+                        compilationRequirementAdditionalSignatureData = additionalSignatureData + "|\(signature)"
+                    } catch {
+                        delegate.error("failed to serialize 'MODULE_DEPENDENCIES' context information: \(error)")
+                        return
+                    }
+                }
+                else {
+                    compilationRequirementAdditionalSignatureData = additionalSignatureData
+                }
+
                 // Compilation Requirements
-                delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo("SwiftDriver Compilation Requirements", targetName), additionalSignatureData: additionalSignatureData, commandLine: ["builtin-Swift-Compilation-Requirements", "--"] + args, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: allInputsNodes, outputs: compilationRequirementOutputs, action: delegate.taskActionCreationDelegate.createSwiftCompilationRequirementTaskAction(), execDescription: archSpecificExecutionDescription(cbc.scope.namespace.parseString("Unblock downstream dependents of $PRODUCT_NAME"), cbc, delegate), preparesForIndexing: true, enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilation, .compilationRequirement, .linkingRequirement, .blockedByTargetHeaders, .compilationForIndexableSourceFile], usesExecutionInputs: true, showInLog: true)
+                delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo("SwiftDriver Compilation Requirements", targetName), additionalSignatureData: compilationRequirementAdditionalSignatureData, commandLine: ["builtin-Swift-Compilation-Requirements", "--"] + args, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: allInputsNodes, outputs: compilationRequirementOutputs, action: delegate.taskActionCreationDelegate.createSwiftCompilationRequirementTaskAction(), execDescription: archSpecificExecutionDescription(cbc.scope.namespace.parseString("Unblock downstream dependents of $PRODUCT_NAME"), cbc, delegate), preparesForIndexing: true, enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilation, .compilationRequirement, .linkingRequirement, .blockedByTargetHeaders, .compilationForIndexableSourceFile], usesExecutionInputs: true, showInLog: true)
 
                 if case .compile = compilationMode {
                     // Unblocking compilation

From b9a8b86c89617814fe4276ca8f94d5af12935c9a Mon Sep 17 00:00:00 2001
From: Michael Rawdon <rawdon@apple.com>
Date: Tue, 29 Jul 2025 16:38:43 -0700
Subject: [PATCH 22/49] Remove
 IndexDependencyResolutionTests.verifyAllPlatformsHaveDefaultArch()

This test isn't very useful and has become a maintenance burden.
---
 .../IndexTargetDependencyResolverTests.swift       | 14 --------------
 1 file changed, 14 deletions(-)

diff --git a/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift b/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
index c787f3f7..e50af26a 100644
--- a/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
+++ b/Tests/SWBCoreTests/IndexTargetDependencyResolverTests.swift
@@ -1070,20 +1070,6 @@ import SWBUtil
             "driver-driverkit": "arm64",
         ])
     }
-
-    @Test
-    func verifyAllPlatformsHaveDefaultArch() async throws {
-        let core = try await getCore()
-
-        var defaultArchs: [String: String] = [:]
-        for platform in core.platformRegistry.platforms {
-            let archName: String? = platform.determineDefaultArchForIndexArena(preferredArch: "unknown_arch", using: core)
-            defaultArchs[platform.name] = archName
-            #expect(archName != nil, "'\(platform.identifier)' is missing a default architecture for indexing")
-        }
-
-        //attach(name: "Index Platform Architecture Defaults", plistObject: defaultArchs, lifetime: .keepAlways)
-    }
 }
 
 extension TargetGraph {

From 81963980c38a84edff00249da2f68e12390e0cf9 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Wed, 30 Jul 2025 21:33:53 -0700
Subject: [PATCH 23/49] Upstream trivial diffs

withSerializationContext needs to be exposed to tests, and BuildService.run needs to build and do so without warnings regardless of whether USE_STATIC_PLUGIN_INITIALIZATION is set. EXCLUDED_RECURSIVE_SEARCH_PATH_SUBDIRECTORIES's DefaultValue was duplicated in an incorrect merge conflict resolution. STRING_CATALOG_GENERATE_SYMBOLS was missing from the CoreBuildSystem xcspec.
---
 Sources/SWBBuildService/BuildServiceEntryPoint.swift | 2 +-
 Sources/SWBCore/Specs/CoreBuildSystem.xcspec         | 8 +++++++-
 Sources/SWBTaskExecution/BuildDescription.swift      | 2 +-
 3 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/Sources/SWBBuildService/BuildServiceEntryPoint.swift b/Sources/SWBBuildService/BuildServiceEntryPoint.swift
index 1a66fff2..6614cf42 100644
--- a/Sources/SWBBuildService/BuildServiceEntryPoint.swift
+++ b/Sources/SWBBuildService/BuildServiceEntryPoint.swift
@@ -95,7 +95,7 @@ extension BuildService {
     ///
     /// Called directly from the exported C entry point `swiftbuildServiceEntryPoint` for in-process connections, or from `BuildService.main()` (after some basic file descriptor setup) for out-of-process connections.
     fileprivate static func run(inputFD: FileDescriptor, outputFD: FileDescriptor, connectionMode: ServiceHostConnectionMode, pluginsDirectory: URL?, arguments: [String], pluginLoadingFinished: () throws -> Void) async throws {
-        let pluginManager = await { @PluginExtensionSystemActor in
+        let pluginManager = try await { @PluginExtensionSystemActor () async throws in
             // Create the plugin manager and load plugins.
             let pluginManager = PluginManager(skipLoadingPluginIdentifiers: [])
 
diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
index f71e8276..467e061b 100644
--- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
+++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
@@ -1195,7 +1195,6 @@ When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [CFBundleIdenti
                 Type = StringList;
                 Category = "Search Paths";
                 DefaultValue = "*.nib *.lproj *.framework *.gch *.xcode* *.xcassets *.icon (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj";
-                DefaultValue = "*.nib *.lproj *.framework *.gch *.xcode* *.xcassets (*) .DS_Store CVS .svn .git .hg *.pbproj *.pbxproj";
             },
             {
                 Name = "VERSIONING_SYSTEM";
@@ -3652,6 +3651,13 @@ When this setting is enabled:
                 Category = "Localization";
                 Description = "When enabled, string tables generated in a localization export will prefer the String Catalog format.";
             },
+            {   Name = STRING_CATALOG_GENERATE_SYMBOLS;
+                Type = Boolean;
+                DefaultValue = NO;
+                DisplayName = "Generate String Catalog Symbols";
+                Category = "Localization";
+                Description = "When enabled, symbols will be generated for manually-managed strings in String Catalogs.";
+            },
 
             // rdar://108915072 (Move Siri Category and relevant configs out of CoreBuildSystem)
             // Siri Settings
diff --git a/Sources/SWBTaskExecution/BuildDescription.swift b/Sources/SWBTaskExecution/BuildDescription.swift
index 632a40d8..53aae732 100644
--- a/Sources/SWBTaskExecution/BuildDescription.swift
+++ b/Sources/SWBTaskExecution/BuildDescription.swift
@@ -1398,7 +1398,7 @@ package final class TaskActionRegistry: Sendable {
         implementations = try TaskActionExtensionPoint.taskActionImplementations(pluginManager: pluginManager)
     }
 
-    func withSerializationContext<T>(_ block: () throws -> T) rethrows -> T {
+    @_spi(Testing) public func withSerializationContext<T>(_ block: () throws -> T) rethrows -> T {
         try TaskAction.$taskActionImplementations.withValue(implementations) {
             try block()
         }

From f6d3d4f6f2b8ce29e66faddc4ff0dcf5d6bf8535 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20B=C3=BCgling?= <bbuegling@apple.com>
Date: Thu, 31 Jul 2025 13:02:31 -0700
Subject: [PATCH 24/49] Centralize dependency diagnostics in
 `ValidateDependencies` action (#675)

Instead of emitting diagnostics directly, individual tasks emit .dependencies files which are read by a per-target action which aggregates them. This avoids duplication of diagnostics across tasks.

rdar://156174696
---
 Sources/SWBCore/CMakeLists.txt                |  1 +
 Sources/SWBCore/Dependencies.swift            | 26 +++++-
 Sources/SWBCore/PlannedTaskAction.swift       |  1 +
 .../SpecImplementations/RegisterSpecs.swift   |  1 +
 .../SpecImplementations/Tools/CCompiler.swift | 23 +++--
 .../Tools/SwiftCompiler.swift                 | 36 ++++++--
 .../Tools/ValidateDependencies.swift          | 53 ++++++++++++
 Sources/SWBCore/TaskGeneration.swift          |  2 +
 .../SourcesTaskProducer.swift                 | 17 ++++
 .../TaskProducers/TaskProducer.swift          |  2 +
 .../BuildDescriptionManager.swift             |  4 +
 .../BuiltinTaskActionsExtension.swift         |  3 +-
 Sources/SWBTaskExecution/CMakeLists.txt       |  1 +
 .../TaskActions/ClangCompileTaskAction.swift  | 25 +++---
 .../TaskActions/SwiftDriverTaskAction.swift   | 22 +++--
 .../ValidateDependenciesTaskAction.swift      | 84 +++++++++++++++++++
 .../CapturingTaskGenerationDelegate.swift     |  4 +
 .../SWBTestSupport/DummyCommandProducer.swift |  2 +
 .../TaskPlanningTestSupport.swift             |  4 +
 .../DependencyValidationTests.swift           | 25 +++---
 .../CommandLineSpecPerfTests.swift            |  4 +
 21 files changed, 297 insertions(+), 43 deletions(-)
 create mode 100644 Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
 create mode 100644 Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift

diff --git a/Sources/SWBCore/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt
index 3833e042..93ceda77 100644
--- a/Sources/SWBCore/CMakeLists.txt
+++ b/Sources/SWBCore/CMakeLists.txt
@@ -161,6 +161,7 @@ add_library(SWBCore
   SpecImplementations/Tools/TiffUtilTool.swift
   SpecImplementations/Tools/TouchTool.swift
   SpecImplementations/Tools/UnifdefTool.swift
+  SpecImplementations/Tools/ValidateDependencies.swift
   SpecImplementations/Tools/ValidateDevelopmentAssets.swift
   SpecImplementations/Tools/ValidateEmbeddedBinaryTool.swift
   SpecImplementations/Tools/ValidateProductTool.swift
diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index 0429c616..64824303 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -63,7 +63,7 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
 }
 
 public struct ModuleDependenciesContext: Sendable, SerializableCodable {
-    var validate: BooleanWarningLevel
+    public var validate: BooleanWarningLevel
     var moduleDependencies: [ModuleDependency]
     var fixItContext: FixItContext?
 
@@ -235,3 +235,27 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
         }
     }
 }
+
+public struct DependencyValidationInfo: Hashable, Sendable, Codable {
+    public struct Import: Hashable, Sendable, Codable {
+        public let dependency: ModuleDependency
+        public let importLocations: [Diagnostic.Location]
+
+        public init(dependency: ModuleDependency, importLocations: [Diagnostic.Location]) {
+            self.dependency = dependency
+            self.importLocations = importLocations
+        }
+    }
+
+    public enum Payload: Hashable, Sendable, Codable {
+        case clangDependencies(files: [String])
+        case swiftDependencies(imports: [Import])
+        case unsupported
+    }
+
+    public let payload: Payload
+
+    public init(payload: Payload) {
+        self.payload = payload
+    }
+}
diff --git a/Sources/SWBCore/PlannedTaskAction.swift b/Sources/SWBCore/PlannedTaskAction.swift
index 3518ddc3..c81f93ee 100644
--- a/Sources/SWBCore/PlannedTaskAction.swift
+++ b/Sources/SWBCore/PlannedTaskAction.swift
@@ -346,6 +346,7 @@ public protocol TaskActionCreationDelegate
     func createSignatureCollectionTaskAction() -> any PlannedTaskAction
     func createClangModuleVerifierInputGeneratorTaskAction() -> any PlannedTaskAction
     func createProcessSDKImportsTaskAction() -> any PlannedTaskAction
+    func createValidateDependenciesTaskAction() -> any PlannedTaskAction
 }
 
 extension TaskActionCreationDelegate {
diff --git a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
index 4bf7277f..db2699f4 100644
--- a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
+++ b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
@@ -126,6 +126,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
             RegisterExecutionPolicyExceptionToolSpec.self,
             SwiftHeaderToolSpec.self,
             TAPIMergeToolSpec.self,
+            ValidateDependenciesSpec.self,
             ValidateDevelopmentAssets.self,
             ConstructStubExecutorFileListToolSpec.self,
             GateSpec.self,
diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
index 28863ecb..460489d1 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
@@ -434,8 +434,9 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
 
     public let moduleDependenciesContext: ModuleDependenciesContext?
     public let traceFilePath: Path?
+    public let dependencyValidationOutputPath: Path?
 
-    fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil) {
+    fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil, dependencyValidationOutputPath: Path? = nil) {
         if let developerPathString, explicitModulesPayload == nil {
             self.dependencyInfoEditPayload = .init(removablePaths: [], removableBasenames: [], developerPath: Path(developerPathString))
         } else {
@@ -448,10 +449,11 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
         self.fileNameMapPath = fileNameMapPath
         self.moduleDependenciesContext = moduleDependenciesContext
         self.traceFilePath = traceFilePath
+        self.dependencyValidationOutputPath = dependencyValidationOutputPath
     }
 
     public func serialize<T: Serializer>(to serializer: T) {
-        serializer.serializeAggregate(8) {
+        serializer.serializeAggregate(9) {
             serializer.serialize(serializedDiagnosticsPath)
             serializer.serialize(indexingPayload)
             serializer.serialize(explicitModulesPayload)
@@ -460,11 +462,12 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
             serializer.serialize(dependencyInfoEditPayload)
             serializer.serialize(moduleDependenciesContext)
             serializer.serialize(traceFilePath)
+            serializer.serialize(dependencyValidationOutputPath)
         }
     }
 
     public init(from deserializer: any Deserializer) throws {
-        try deserializer.beginAggregate(8)
+        try deserializer.beginAggregate(9)
         self.serializedDiagnosticsPath = try deserializer.deserialize()
         self.indexingPayload = try deserializer.deserialize()
         self.explicitModulesPayload = try deserializer.deserialize()
@@ -473,6 +476,7 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
         self.dependencyInfoEditPayload = try deserializer.deserialize()
         self.moduleDependenciesContext = try deserializer.deserialize()
         self.traceFilePath = try deserializer.deserialize()
+        self.dependencyValidationOutputPath = try deserializer.deserialize()
     }
 }
 
@@ -1165,10 +1169,14 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
             dependencyData = nil
         }
 
+        let extraOutputs: [any PlannedNode]
         let moduleDependenciesContext = cbc.producer.moduleDependenciesContext
+        let dependencyValidationOutputPath: Path?
         let traceFilePath: Path?
         if clangInfo?.hasFeature("print-headers-direct-per-file") ?? false,
             (moduleDependenciesContext?.validate ?? .defaultValue) != .no {
+            dependencyValidationOutputPath = Path(outputNode.path.str + ".dependencies")
+
             let file = Path(outputNode.path.str + ".trace.json")
             commandLine += [
                 "-Xclang", "-header-include-file",
@@ -1177,8 +1185,12 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
                 "-Xclang", "-header-include-format=json"
             ]
             traceFilePath = file
+
+            extraOutputs = [MakePlannedPathNode(dependencyValidationOutputPath!), MakePlannedPathNode(traceFilePath!)]
         } else {
+            dependencyValidationOutputPath = nil
             traceFilePath = nil
+            extraOutputs = []
         }
 
         // Add the diagnostics serialization flag.  We currently place the diagnostics file right next to the output object file.
@@ -1293,7 +1305,8 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
             fileNameMapPath: verifierPayload?.fileNameMapPath,
             developerPathString: recordSystemHeaderDepsOutsideSysroot ? cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR).str : nil,
             moduleDependenciesContext: moduleDependenciesContext,
-            traceFilePath: traceFilePath
+            traceFilePath: traceFilePath,
+            dependencyValidationOutputPath: dependencyValidationOutputPath
         )
 
         var inputNodes: [any PlannedNode] = inputDeps.map { delegate.createNode($0) }
@@ -1357,7 +1370,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
         }
 
         // Finally, create the task.
-        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode], action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
+        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
 
         // If the object file verifier is enabled and we are building with explicit modules, also create a job to produce adjacent objects using implicit modules, then compare the results.
         if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil {
diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
index 0bfa6c0b..827eaac7 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
@@ -273,6 +273,16 @@ public struct SwiftSourceFileIndexingInfo: SourceFileIndexingInfo {
     }
 }
 
+public struct SwiftDependencyValidationPayload: SerializableCodable, Encodable, Sendable {
+    public let dependencyValidationOutputPath: Path
+    public let moduleDependenciesContext: ModuleDependenciesContext
+
+    public init(dependencyValidationOutputPath: Path, moduleDependenciesContext: ModuleDependenciesContext) {
+        self.dependencyValidationOutputPath = dependencyValidationOutputPath
+        self.moduleDependenciesContext = moduleDependenciesContext
+    }
+}
+
 /// The minimal data we need to serialize to reconstruct `generatePreviewInfo`
 public struct SwiftPreviewPayload: Serializable, Encodable, Sendable {
     public let architecture: String
@@ -446,9 +456,9 @@ public struct SwiftTaskPayload: ParentTaskPayload {
     /// The preview build style in effect (dynamic replacement or XOJIT), if any.
     public let previewStyle: PreviewStyleMessagePayload?
 
-    public let moduleDependenciesContext: ModuleDependenciesContext?
+    public let dependencyValidationPayload: SwiftDependencyValidationPayload?
 
-    init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, moduleDependenciesContext: ModuleDependenciesContext?) {
+    init(moduleName: String, indexingPayload: SwiftIndexingPayload, previewPayload: SwiftPreviewPayload?, localizationPayload: SwiftLocalizationPayload?, numExpectedCompileSubtasks: Int, driverPayload: SwiftDriverPayload?, previewStyle: PreviewStyle?, dependencyValidationPayload: SwiftDependencyValidationPayload?) {
         self.moduleName = moduleName
         self.indexingPayload = indexingPayload
         self.previewPayload = previewPayload
@@ -463,7 +473,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
         case nil:
             self.previewStyle = nil
         }
-        self.moduleDependenciesContext = moduleDependenciesContext
+        self.dependencyValidationPayload = dependencyValidationPayload
     }
 
     public func serialize<T: Serializer>(to serializer: T) {
@@ -475,7 +485,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
             serializer.serialize(numExpectedCompileSubtasks)
             serializer.serialize(driverPayload)
             serializer.serialize(previewStyle)
-            serializer.serialize(moduleDependenciesContext)
+            serializer.serialize(dependencyValidationPayload)
         }
     }
 
@@ -488,7 +498,7 @@ public struct SwiftTaskPayload: ParentTaskPayload {
         self.numExpectedCompileSubtasks = try deserializer.deserialize()
         self.driverPayload = try deserializer.deserialize()
         self.previewStyle = try deserializer.deserialize()
-        self.moduleDependenciesContext = try deserializer.deserialize()
+        self.dependencyValidationPayload = try deserializer.deserialize()
     }
 }
 
@@ -2283,6 +2293,20 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                 moduleOutputPaths.append(moduleWrapOutput)
             }
 
+            let dependencyValidationPayload: SwiftDependencyValidationPayload?
+            if let context = cbc.producer.moduleDependenciesContext, let outputPath = objectOutputPaths.first, context.validate != .no {
+                let primarySwiftBaseName = cbc.scope.evaluate(BuiltinMacros.TARGET_NAME) + compilationMode.moduleBaseNameSuffix + "-primary"
+                let dependencyValidationOutputPath = outputPath.dirname.join(primarySwiftBaseName + ".dependencies")
+                extraOutputPaths.append(dependencyValidationOutputPath)
+
+                dependencyValidationPayload = .init(
+                    dependencyValidationOutputPath: dependencyValidationOutputPath,
+                    moduleDependenciesContext: context
+                )
+            } else {
+                dependencyValidationPayload = nil
+            }
+
             // The rule info.
             //
             // NOTE: If this changes, be sure to update the log parser to extract the variant and arch properly.
@@ -2312,7 +2336,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                 numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count,
                 driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath),
                 previewStyle: cbc.scope.previewStyle,
-                moduleDependenciesContext: cbc.producer.moduleDependenciesContext
+                dependencyValidationPayload: dependencyValidationPayload
             )
 
             // Finally, assemble the input and output paths and create the Swift compiler command.
diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
new file mode 100644
index 00000000..0fb14969
--- /dev/null
+++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
@@ -0,0 +1,53 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+public import SWBUtil
+import SWBMacro
+
+public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementationType, @unchecked Sendable {
+    public static let identifier = "com.apple.tools.validate-dependencies"
+
+    public static func construct(registry: SpecRegistry, proxy: SpecProxy) -> Spec {
+        return ValidateDependenciesSpec(registry: registry)
+    }
+
+    public init(registry: SpecRegistry) {
+        let proxy = SpecProxy(identifier: Self.identifier, domain: "", path: Path(""), type: Self.self, classType: nil, basedOn: nil, data: ["ExecDescription": PropertyListItem("Validate dependencies")], localizedStrings: nil)
+        super.init(createSpecParser(for: proxy, registry: registry), nil, isGeneric: false)
+    }
+
+    required init(_ parser: SpecParser, _ basedOnSpec: Spec?) {
+        super.init(parser, basedOnSpec, isGeneric: false)
+    }
+
+    override public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
+        fatalError("unexpected direct invocation")
+    }
+
+    public override var payloadType: (any TaskPayload.Type)? { return ValidateDependenciesPayload.self }
+
+    public func createTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, dependencyInfos: [PlannedPathNode], payload: ValidateDependenciesPayload) async {
+        guard let configuredTarget = cbc.producer.configuredTarget else {
+            return
+        }
+        let output =  delegate.createVirtualNode("ValidateDependencies \(configuredTarget.guid)")
+        delegate.createTask(type: self, payload: payload, ruleInfo: ["ValidateDependencies"], commandLine: ["builtin-validate-dependencies"] + dependencyInfos.map { $0.path.str }, environment: EnvironmentBindings(), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: dependencyInfos + cbc.commandOrderingInputs, outputs: [output], action: delegate.taskActionCreationDelegate.createValidateDependenciesTaskAction(), preparesForIndexing: false, enableSandboxing: false)
+    }
+}
+
+public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCodable {
+    public let moduleDependenciesContext: ModuleDependenciesContext
+
+    public init(moduleDependenciesContext: ModuleDependenciesContext) {
+        self.moduleDependenciesContext = moduleDependenciesContext
+    }
+}
diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift
index 82b871d7..744ea2ac 100644
--- a/Sources/SWBCore/TaskGeneration.swift
+++ b/Sources/SWBCore/TaskGeneration.swift
@@ -189,6 +189,8 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
 
     var processSDKImportsSpec: ProcessSDKImportsSpec { get }
 
+    var validateDependenciesSpec: ValidateDependenciesSpec { get }
+
     /// The default working directory to use for a task, if it doesn't have a stronger preference.
     var defaultWorkingDirectory: Path { get }
 
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
index 5987aa71..1c847b6a 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
@@ -758,6 +758,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
         }
 
         var tasks: [any PlannedTask] = []
+        var dependencyDataFiles: [PlannedPathNode] = []
 
         // Generate any auxiliary files whose content is not per-arch or per-variant.
         // For the index build arena it is important to avoid adding this because it forces creation of the Swift module due to the generated ObjC header being an input dependency. This is unnecessary work since we don't need to generate the Swift module of the target to be able to successfully create a compiler AST for the Swift files of the target.
@@ -903,6 +904,8 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
                         case "swiftmodule":
                             dsymutilInputNodes.append(object)
                             break
+                        case "dependencies":
+                            dependencyDataFiles.append(MakePlannedPathNode(object.path))
                         default:
                             break
                         }
@@ -1594,6 +1597,20 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
             tasks = tasks.filter { $0.inputs.contains(where: { $0.path.isValidLocalizedContent(scope) || $0.path.fileExtension == "xcstrings" }) }
         }
 
+        // Create a task to validate dependencies if that feature is enabled.
+        if let moduleDependenciesContext = context.moduleDependenciesContext, moduleDependenciesContext.validate != .no {
+            var validateDepsTasks = [any PlannedTask]()
+            await appendGeneratedTasks(&validateDepsTasks, usePhasedOrdering: true) { delegate in
+                await context.validateDependenciesSpec.createTasks(
+                    CommandBuildContext(producer: context, scope: scope, inputs: []),
+                    delegate,
+                    dependencyInfos: dependencyDataFiles,
+                    payload: .init(moduleDependenciesContext: moduleDependenciesContext)
+                )
+            }
+            tasks.append(contentsOf: validateDepsTasks)
+        }
+
         return tasks
     }
 
diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
index 82fd7837..2c481238 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
@@ -282,6 +282,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
     let validateProductSpec: ValidateProductToolSpec
     let processXCFrameworkLibrarySpec: ProcessXCFrameworkLibrarySpec
     public let processSDKImportsSpec: ProcessSDKImportsSpec
+    public let validateDependenciesSpec: ValidateDependenciesSpec
     public let writeFileSpec: WriteFileSpec
     private let _documentationCompilerSpec: Result<CommandLineToolSpec, any Error>
     var documentationCompilerSpec: CommandLineToolSpec? { return specForResult(_documentationCompilerSpec) }
@@ -404,6 +405,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
         self.validateProductSpec = workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.platform.validate", domain: domain) as! ValidateProductToolSpec
         self.processXCFrameworkLibrarySpec = workspaceContext.core.specRegistry.getSpec(ProcessXCFrameworkLibrarySpec.identifier, domain: domain) as! ProcessXCFrameworkLibrarySpec
         self.processSDKImportsSpec = workspaceContext.core.specRegistry.getSpec(ProcessSDKImportsSpec.identifier, domain: domain) as! ProcessSDKImportsSpec
+        self.validateDependenciesSpec = workspaceContext.core.specRegistry.getSpec(ValidateDependenciesSpec.identifier, domain: domain) as! ValidateDependenciesSpec
         self.writeFileSpec = workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.write-file", domain: domain) as! WriteFileSpec
         self._documentationCompilerSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.documentation", domain: domain) as CommandLineToolSpec }
         self._tapiSymbolExtractorSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.documentation.objc-symbol-extract", domain: domain) as TAPISymbolExtractor }
diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
index c6a57ead..667e0e44 100644
--- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift
+++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
@@ -891,6 +891,10 @@ extension BuildSystemTaskPlanningDelegate: TaskActionCreationDelegate {
     func createProcessSDKImportsTaskAction() -> any PlannedTaskAction {
         return ProcessSDKImportsTaskAction()
     }
+
+    func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
+        return ValidateDependenciesTaskAction()
+    }
 }
 
 fileprivate extension BuildDescription {
diff --git a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
index 6474dd83..fa3b298b 100644
--- a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
+++ b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
@@ -51,7 +51,8 @@ public struct BuiltinTaskActionsExtension: TaskActionExtension {
             36: ConstructStubExecutorInputFileListTaskAction.self,
             37: ConcatenateTaskAction.self,
             38: GenericCachingTaskAction.self,
-            39: ProcessSDKImportsTaskAction.self
+            39: ProcessSDKImportsTaskAction.self,
+            40: ValidateDependenciesTaskAction.self,
         ]
     }
 }
diff --git a/Sources/SWBTaskExecution/CMakeLists.txt b/Sources/SWBTaskExecution/CMakeLists.txt
index 6c7cba03..4a766605 100644
--- a/Sources/SWBTaskExecution/CMakeLists.txt
+++ b/Sources/SWBTaskExecution/CMakeLists.txt
@@ -70,6 +70,7 @@ add_library(SWBTaskExecution
   TaskActions/SwiftDriverTaskAction.swift
   TaskActions/SwiftHeaderToolTaskAction.swift
   TaskActions/TaskAction.swift
+  TaskActions/ValidateDependenciesTaskAction.swift
   TaskActions/ValidateDevelopmentAssetsTaskAction.swift
   TaskActions/ValidateProductTaskAction.swift
   TaskResult.swift
diff --git a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
index d72d198e..88062d09 100644
--- a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
@@ -251,12 +251,15 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
             // Check if verifying dependencies from trace data is enabled.
             let traceFilePath: Path?
             let moduleDependenciesContext: ModuleDependenciesContext?
+            let dependencyValidationOutputPath: Path?
             if let payload = task.payload as? ClangTaskPayload {
                 traceFilePath = payload.traceFilePath
                 moduleDependenciesContext = payload.moduleDependenciesContext
+                dependencyValidationOutputPath = payload.dependencyValidationOutputPath
             } else {
                 traceFilePath = nil
                 moduleDependenciesContext = nil
+                dependencyValidationOutputPath = nil
             }
             if let traceFilePath {
                 // Remove the trace output file if it already exists.
@@ -322,26 +325,28 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
                 }
             }
 
-            if let moduleDependenciesContext, lastResult == .succeeded {
+            if lastResult == .succeeded {
                 // Verify the dependencies from the trace data.
-                let files: [Path]?
+                let payload: DependencyValidationInfo.Payload
                 if let traceFilePath {
                     let fs = executionDelegate.fs
                     let traceData = try JSONDecoder().decode(Array<TraceData>.self, from: Data(fs.read(traceFilePath)))
 
                     var allFiles = Set<Path>()
                     traceData.forEach { allFiles.formUnion(Set($0.includes)) }
-                    files = Array(allFiles)
+                    payload = .clangDependencies(files: allFiles.map { $0.str })
                 } else {
-                    files = nil
-                }
-                let diagnostics = moduleDependenciesContext.makeDiagnostics(files: files)
-                for diagnostic in diagnostics {
-                    outputDelegate.emit(diagnostic)
+                    payload = .unsupported
                 }
 
-                if diagnostics.contains(where: { $0.behavior == .error }) {
-                    return .failed
+                if let dependencyValidationOutputPath {
+                    let validationInfo = DependencyValidationInfo(payload: payload)
+                    _ = try executionDelegate.fs.writeIfChanged(
+                        dependencyValidationOutputPath,
+                        contents: ByteString(
+                            JSONEncoder(outputFormatting: .sortedKeys).encode(validationInfo)
+                        )
+                    )
                 }
             }
 
diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift
index 6b6b5a1a..082945bd 100644
--- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverTaskAction.swift
@@ -96,17 +96,21 @@ final public class SwiftDriverTaskAction: TaskAction, BuildValueValidatingTaskAc
             }
 
             if driverPayload.explicitModulesEnabled,
-               let moduleDependenciesContext = payload.moduleDependenciesContext
+               let dependencyValidationPayload = payload.dependencyValidationPayload
             {
-                let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID)
-                let diagnostics = moduleDependenciesContext.makeDiagnostics(imports: imports)
-                for diagnostic in diagnostics {
-                    outputDelegate.emit(diagnostic)
-                }
-
-                if (diagnostics.contains { $0.behavior == .error }) {
-                    return .failed
+                let payload: DependencyValidationInfo.Payload
+                if let imports = try await dependencyGraph.mainModuleImportModuleDependencies(for: driverPayload.uniqueID) {
+                    payload = .swiftDependencies(imports: imports.map { .init(dependency: $0.0, importLocations: $0.importLocations) })
+                } else {
+                    payload = .unsupported
                 }
+                let validationInfo = DependencyValidationInfo(payload: payload)
+                _ = try executionDelegate.fs.writeIfChanged(
+                    dependencyValidationPayload.dependencyValidationOutputPath,
+                    contents: ByteString(
+                        JSONEncoder(outputFormatting: .sortedKeys).encode(validationInfo)
+                    )
+                )
             }
 
             if driverPayload.reportRequiredTargetDependencies != .no && driverPayload.explicitModulesEnabled, let target = task.forTarget {
diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
new file mode 100644
index 00000000..313262ab
--- /dev/null
+++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
@@ -0,0 +1,84 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+public import SWBCore
+import SWBUtil
+
+public final class ValidateDependenciesTaskAction: TaskAction {
+    public override class var toolIdentifier: String {
+        return "validate-dependencies"
+    }
+
+    public override func performTaskAction(_ task: any ExecutableTask, dynamicExecutionDelegate: any DynamicTaskExecutionDelegate, executionDelegate: any TaskExecutionDelegate, clientDelegate: any TaskExecutionClientDelegate, outputDelegate: any TaskOutputDelegate) async -> CommandResult {
+        let commandLine = Array(task.commandLineAsStrings)
+        guard commandLine.count >= 1, commandLine[0] == "builtin-validate-dependencies" else {
+            outputDelegate.emitError("unexpected arguments: \(commandLine)")
+            return .failed
+        }
+
+        guard let context = (task.payload as? ValidateDependenciesPayload)?.moduleDependenciesContext else {
+            if let payload = task.payload {
+                outputDelegate.emitError("invalid task payload: \(payload)")
+            } else {
+                outputDelegate.emitError("empty task payload")
+            }
+            return .failed
+        }
+
+        do {
+            var allFiles = Set<String>()
+            var allImports = Set<DependencyValidationInfo.Import>()
+            var unsupported = false
+
+            for inputPath in task.inputPaths {
+                let inputData = try Data(contentsOf: URL(fileURLWithPath: inputPath.str))
+                let info = try JSONDecoder().decode(DependencyValidationInfo.self, from: inputData)
+
+                switch info.payload {
+                case .clangDependencies(let files):
+                    files.forEach {
+                        allFiles.insert($0)
+                    }
+                case .swiftDependencies(let imports):
+                    imports.forEach {
+                        allImports.insert($0)
+                    }
+                case .unsupported:
+                    unsupported = true
+                }
+            }
+
+            var diagnostics: [Diagnostic] = []
+
+            if unsupported {
+                diagnostics.append(contentsOf: context.makeDiagnostics(files: nil))
+            } else {
+                diagnostics.append(contentsOf: context.makeDiagnostics(files: allFiles.map { Path($0) }))
+                diagnostics.append(contentsOf: context.makeDiagnostics(imports: allImports.map { ($0.dependency, $0.importLocations) }))
+            }
+
+            for diagnostic in diagnostics {
+                outputDelegate.emit(diagnostic)
+            }
+
+            if diagnostics.contains(where: { $0.behavior == .error }) {
+                return .failed
+            }
+        } catch {
+            outputDelegate.emitError("\(error)")
+            return .failed
+        }
+
+        return .succeeded
+    }
+}
diff --git a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
index b4689254..cc8c6eee 100644
--- a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
+++ b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
@@ -239,4 +239,8 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
     package func createProcessSDKImportsTaskAction() -> any PlannedTaskAction {
         return ProcessSDKImportsTaskAction()
     }
+
+    package func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
+        return ValidateDependenciesTaskAction()
+    }
 }
diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift
index a208163b..9d00f0b5 100644
--- a/Sources/SWBTestSupport/DummyCommandProducer.swift
+++ b/Sources/SWBTestSupport/DummyCommandProducer.swift
@@ -104,6 +104,7 @@ package struct MockCommandProducer: CommandProducer, Sendable {
         self.mkdirSpec = try getSpec("com.apple.tools.mkdir") as MkdirToolSpec
         self.swiftCompilerSpec = try getSpec() as SwiftCompilerSpec
         self.processSDKImportsSpec = try getSpec(ProcessSDKImportsSpec.identifier) as ProcessSDKImportsSpec
+        self.validateDependenciesSpec = try getSpec(ValidateDependenciesSpec.identifier) as ValidateDependenciesSpec
     }
 
     package let specDataCaches = Registry<Spec, any SpecDataCache>()
@@ -140,6 +141,7 @@ package struct MockCommandProducer: CommandProducer, Sendable {
     package let mkdirSpec: MkdirToolSpec
     package let swiftCompilerSpec: SwiftCompilerSpec
     package let processSDKImportsSpec: ProcessSDKImportsSpec
+    package let validateDependenciesSpec: ValidateDependenciesSpec
 
     package var defaultWorkingDirectory: Path {
         return Path("/tmp")
diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
index f615bdc7..0fdacfea 100644
--- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
+++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
@@ -467,6 +467,10 @@ extension TestTaskPlanningDelegate: TaskActionCreationDelegate {
     package func createProcessSDKImportsTaskAction() -> any PlannedTaskAction {
         return ProcessSDKImportsTaskAction()
     }
+
+    package func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
+        return ValidateProductTaskAction()
+    }
 }
 
 package final class CancellingTaskPlanningDelegate: TestTaskPlanningDelegate, @unchecked Sendable {
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 85204c76..d1aedee3 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -327,7 +327,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
         }
     }
 
-    @Test(.requireSDKs(.host))
+    @Test(.requireSDKs(.host), .skipHostOS(.windows, "toolchain too old"), .skipHostOS(.linux, "toolchain too old"))
     func validateModuleDependenciesSwift() async throws {
         try await withTemporaryDirectory { tmpDir in
             let testWorkspace = try await TestWorkspace(
@@ -471,7 +471,8 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                         groupTree: TestGroup(
                             "Sources", path: "Sources",
                             children: [
-                                TestFile("CoreFoo.m")
+                                TestFile("CoreFoo.m"),
+                                TestFile("CoreBar.m"),
                             ]),
                         buildConfigurations: [
                             TestBuildConfiguration(
@@ -493,7 +494,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                             TestStandardTarget(
                                 "CoreFoo", type: .framework,
                                 buildPhases: [
-                                    TestSourcesBuildPhase(["CoreFoo.m"]),
+                                    TestSourcesBuildPhase(["CoreFoo.m", "CoreBar.m"]),
                                     TestFrameworksBuildPhase()
                                 ])
                         ])
@@ -504,14 +505,16 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
             let SRCROOT = testWorkspace.sourceRoot.join("aProject")
 
             // Write the source files.
-            try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in
-                contents <<< """
-                    #include <Foundation/Foundation.h>
-                    #include <Foundation/NSObject.h>
-                    #include <Accelerate/Accelerate.h>
-
-                    void f0(void) { };
-                """
+            for stem in ["Foo", "Bar"] {
+                try await tester.fs.writeFileContents(SRCROOT.join("Sources/Core\(stem).m")) { contents in
+                    contents <<< """
+                        #include <Foundation/Foundation.h>
+                        #include <Foundation/NSObject.h>
+                        #include <Accelerate/Accelerate.h>
+
+                        void f\(stem)(void) { };
+                    """
+                }
             }
 
             // Expect complaint about undeclared dependency
diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
index d85ccb2c..b7669be0 100644
--- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
+++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
@@ -233,6 +233,10 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
     func createProcessSDKImportsTaskAction() -> any PlannedTaskAction {
         return ProcessSDKImportsTaskAction()
     }
+
+    func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
+        return ValidateProductTaskAction()
+    }
 }
 
 extension CapturingTaskGenerationDelegate: CoreClientDelegate {

From 5aab44a1a8eb4b7ae2cfb0330f9bfafafe1d7c20 Mon Sep 17 00:00:00 2001
From: Rauhul Varma <rauhul.varma@gmail.com>
Date: Thu, 31 Jul 2025 17:50:55 -0700
Subject: [PATCH 25/49] Avoid error if missing xattr support (#690)

When building on a darwin host (e.g. macOS) using a file-system that
doesn't support xattrs (e.g. tmpfs) swift-build would fail trying to set
the "CreatedByBuildSystem" xattribute. This commit changes swift-build
to warn instead, allowing the build to complete.
---
 Sources/SWBCore/WorkspaceContext.swift         | 18 +++++++++++++-----
 .../CreateBuildDirectoryTaskAction.swift       | 14 ++++++++++++--
 2 files changed, 25 insertions(+), 7 deletions(-)

diff --git a/Sources/SWBCore/WorkspaceContext.swift b/Sources/SWBCore/WorkspaceContext.swift
index 1b61e537..41164a8d 100644
--- a/Sources/SWBCore/WorkspaceContext.swift
+++ b/Sources/SWBCore/WorkspaceContext.swift
@@ -401,13 +401,21 @@ extension FSProxy {
         return try getExtendedAttribute(path, key: Self.CreatedByBuildSystemAttribute) == Self.CreatedByBuildSystemAttributeOnValue
     }
 
+    /// Sets the "CreatedByBuildSystem" extended attribute on the specified
+    /// path.
+    ///
+    /// - Important: The caller is responsible for catching and handling
+    ///   failures if the filesystem does not support extended attributes. Many
+    ///   filesystems (e.g. various non-ext4 temporary filesystems on Linux)
+    ///   don't support xattrs and will return `ENOTSUP`. In particular, tmpfs
+    ///   doesn't support xattrs on Linux unless `CONFIG_TMPFS_XATTR` is enabled
+    ///   in the kernel config.
+    ///
+    /// - Parameter path: The path on which to set the extended attribute.
+    /// - Throws: An error if the operation fails, including when the filesystem
+    ///   doesn't support extended attributes.
     public func setCreatedByBuildSystemAttribute(_ path: Path) throws {
-        // Many filesystems on other platforms (e.g. various non-ext4 temporary filesystems on Linux) don't support xattrs and will return ENOTSUP.
-        // In particular, tmpfs doesn't support xattrs on Linux unless `CONFIG_TMPFS_XATTR` is enabled in the kernel config.
-        // FIXME: Detect whether the FS supports xattrs at runtime
-        #if canImport(Darwin)
         try setExtendedAttribute(path, key: Self.CreatedByBuildSystemAttribute, value: Self.CreatedByBuildSystemAttributeOnValue)
-        #endif
     }
 
     public func commandLineArgumentsToApplyCreatedByBuildSystemAttribute(to path: Path) -> [String] {
diff --git a/Sources/SWBTaskExecution/TaskActions/CreateBuildDirectoryTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/CreateBuildDirectoryTaskAction.swift
index 10cc6af5..664383ab 100644
--- a/Sources/SWBTaskExecution/TaskActions/CreateBuildDirectoryTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/CreateBuildDirectoryTaskAction.swift
@@ -28,12 +28,22 @@ public final class CreateBuildDirectoryTaskAction: TaskAction {
 
         do {
             try fs.createDirectory(directoryPath, recursive: true)
-            try fs.setCreatedByBuildSystemAttribute(directoryPath)
-            return .succeeded
         } catch {
             outputDelegate?.emitError(error.localizedDescription)
             return .failed
         }
+
+        // Warn on Darwin if we are unable to set the "CreatedByBuildSystem"
+        // attribute
+        do {
+            try fs.setCreatedByBuildSystemAttribute(directoryPath)
+        } catch {
+            #if canImport(Darwin)
+            outputDelegate?.emitWarning("Failed to set build system attribute on \(directoryPath.str): \(error.localizedDescription)")
+            #endif
+        }
+
+        return .succeeded
     }
 
     public override func performTaskAction(

From d711bb777dcbb36e00132ee54b741893b6c6c192 Mon Sep 17 00:00:00 2001
From: Mirza Garibovic <garibovic@apple.com>
Date: Thu, 31 Jul 2025 14:14:36 -0700
Subject: [PATCH 26/49] Module dependencies fixit should append instead of
 prepend new insertions to xcconfigs (rdar://156174282)

---
 Sources/SWBCore/Dependencies.swift                        | 4 ++--
 Tests/SWBBuildSystemTests/DependencyValidationTests.swift | 6 +++---
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index 64824303..f8209524 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -205,10 +205,10 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
                 self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment)
             }
             else if let path = settings.constructionComponents.targetXcconfigPath {
-                self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: nil))
+                self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: nil))
             }
             else if let path = settings.constructionComponents.projectXcconfigPath {
-                self.init(sourceRange: .init(path: path, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0), modificationStyle: .insertNewAssignment(targetNameCondition: target.name))
+                self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: target.name))
             }
             else {
                 return nil
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index d1aedee3..02a51b51 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -420,11 +420,11 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                 "TargetB": [
                     Diagnostic(
                         behavior: .error,
-                        location: Diagnostic.Location.path(projectXCConfigPath, line: 0, column: 0),
+                        location: Diagnostic.Location.path(projectXCConfigPath, line: .max, column: .max),
                         data: DiagnosticData("Missing entries in MODULE_DEPENDENCIES: Foundation"),
                         fixIts: [
                             Diagnostic.FixIt(
-                                sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0),
+                                sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max),
                                 newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n"),
                         ],
                         childDiagnostics: [
@@ -433,7 +433,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                                 location: Diagnostic.Location.path(swiftSourcePath, line: 1, column: 8),
                                 data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"),
                                 fixIts: [Diagnostic.FixIt(
-                                    sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 0, startColumn: 0, endLine: 0, endColumn: 0),
+                                    sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max),
                                     newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n")],
                             ),
                         ]),

From 12952a3256b95055a39493b0344a10662989fe61 Mon Sep 17 00:00:00 2001
From: Mirza Garibovic <garibovic@apple.com>
Date: Wed, 9 Jul 2025 11:12:52 -0700
Subject: [PATCH 27/49] Dependencies: emit warning instead of error if the
 Swift toolchain can't support VALIDATE_MODULE_DEPENDENCIES to match clang

---
 Sources/SWBCore/Dependencies.swift                        | 2 +-
 Tests/SWBBuildSystemTests/DependencyValidationTests.swift | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index f8209524..a05f33e3 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -138,7 +138,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
         guard validate != .no else { return [] }
         guard let imports else {
             return [Diagnostic(
-                behavior: .error,
+                behavior: .warning,
                 location: .unknown,
                 data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
         }
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 02a51b51..8c156951 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -446,7 +446,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                 let buildRequest = BuildRequest(parameters: parameters, buildTargets: [BuildRequest.BuildTargetInfo(parameters: parameters, target: target)], continueBuildingAfterErrors: false, useParallelTargets: true, useImplicitDependencies: true, useDryRun: false)
 
                 try await tester.checkBuild(runDestination: .host, buildRequest: buildRequest, persistent: true) { results in
-                    guard !results.checkError(.prefix("The current toolchain does not support VALIDATE_MODULE_DEPENDENCIES"), failIfNotFound: false) else { return }
+                    guard !results.checkWarning(.prefix("The current toolchain does not support VALIDATE_MODULE_DEPENDENCIES"), failIfNotFound: false) else { return }
 
                     for expectedDiag in expectedDiags {
                         _ = results.check(.contains(expectedDiag.data.description), kind: expectedDiag.behavior, failIfNotFound: true, sourceLocation: #_sourceLocation) { diag in

From 4f8a345664470bdc010d6d3941dc938270bbb10e Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Thu, 31 Jul 2025 02:58:49 -0700
Subject: [PATCH 28/49] Allow command line tool specs to be extended by
 BuildSettingsExtension specs and an additional task producer extension point

This can allow more decoupling of platform specific concepts and the like from the core build specs.
---
 .../AssetCatalogCompiler.swift                |  7 ++-
 .../InterfaceBuilderCompiler.swift            |  7 ++-
 Sources/SWBApplePlatform/Plugin.swift         |  4 +-
 Sources/SWBCore/Core.swift                    |  1 +
 Sources/SWBCore/Settings/Settings.swift       |  6 +-
 .../CommandLineToolSpec.swift                 | 40 +++++++++-----
 .../PropertyDomainSpec.swift                  | 38 ++++++++++---
 .../SpecImplementations/RegisterSpecs.swift   |  1 +
 .../SWBCore/SpecImplementations/Specs.swift   | 14 +++++
 .../SpecImplementations/Tools/CCompiler.swift | 16 +++++-
 .../Tools/LinkerTools.swift                   |  7 ++-
 .../Tools/SwiftCompiler.swift                 |  2 +-
 .../SpecImplementations/Tools/TAPITools.swift |  4 +-
 Sources/SWBCore/TaskGeneration.swift          | 55 +++++++++++++++++++
 .../ProductPlanning/BuildPlan.swift           |  9 ++-
 .../ProductPlanning/ProductPlanner.swift      |  2 +-
 .../FilesBasedBuildPhaseTaskProducer.swift    |  4 +-
 .../SourcesTaskProducer.swift                 | 10 ++--
 .../TaskProducers/TaskProducer.swift          |  2 +-
 .../TaskProducerExtensionPoint.swift          |  2 +
 Sources/SWBUniversalPlatform/Plugin.swift     |  3 +
 .../CommandLineSpecPerfTests.swift            |  2 +-
 22 files changed, 188 insertions(+), 48 deletions(-)

diff --git a/Sources/SWBApplePlatform/AssetCatalogCompiler.swift b/Sources/SWBApplePlatform/AssetCatalogCompiler.swift
index c253b047..c80ebc6c 100644
--- a/Sources/SWBApplePlatform/AssetCatalogCompiler.swift
+++ b/Sources/SWBApplePlatform/AssetCatalogCompiler.swift
@@ -239,7 +239,12 @@ public final class ActoolCompilerSpec : GenericCompilerSpec, SpecIdentifierType,
         // which it currently receives a listing of via the assetcatalog_dependencies file produced by actool.
         let carFiles = [cbc.resourcesDir?.join("Assets.car")].compactMap { $0 }.map(delegate.createNode)
 
-        let outputs = evaluatedOutputsResult + (additionalEvaluatedOutputsResult.outputs ).map(delegate.createNode)
+        let outputs = evaluatedOutputsResult + (additionalEvaluatedOutputsResult.outputs).map { output in
+            if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
+                delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
+            }
+            return delegate.createNode(output.path)
+        }
         guard !outputs.isEmpty else { preconditionFailure("ActoolCompilerSpec.constructTasks() invoked with no outputs defined") }
 
         let assetSymbolInputs = cbc.inputs
diff --git a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift
index c2d99b00..1221052d 100644
--- a/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift
+++ b/Sources/SWBApplePlatform/InterfaceBuilderCompiler.swift
@@ -86,7 +86,12 @@ public class IbtoolCompilerSpec : GenericCompilerSpec, IbtoolCompilerSupport, @u
 
         // Add the additional outputs defined by the spec.  These are not declared as outputs but should be processed by the tool separately.
         let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate)
-        outputs += additionalEvaluatedOutputsResult.outputs.map(delegate.createNode)
+        outputs += additionalEvaluatedOutputsResult.outputs.map { output in
+            if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
+                delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
+            }
+            return delegate.createNode(output.path)
+        }
 
         if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent {
             delegate.declareGeneratedInfoPlistContent(infoPlistContent)
diff --git a/Sources/SWBApplePlatform/Plugin.swift b/Sources/SWBApplePlatform/Plugin.swift
index adca6f0e..ae11f5c7 100644
--- a/Sources/SWBApplePlatform/Plugin.swift
+++ b/Sources/SWBApplePlatform/Plugin.swift
@@ -37,7 +37,6 @@ struct AppleDeveloperDirectoryExtension: DeveloperDirectoryExtension {
 }
 
 struct TaskProducersExtension: TaskProducerExtension {
-
     func createPreSetupTaskProducers(_ context: TaskProducerContext) -> [any TaskProducer] {
         [DevelopmentAssetsTaskProducer(context)]
     }
@@ -61,6 +60,9 @@ struct TaskProducersExtension: TaskProducerExtension {
     var globalTaskProducers: [any GlobalTaskProducerFactory] {
         [StubBinaryTaskProducerFactory()]
     }
+
+    func generateAdditionalTasks(_ tasks: inout [any SWBCore.PlannedTask], _ producer: any SWBTaskConstruction.TaskProducer) {
+    }
 }
 
 struct ExtensionPointExtractorTaskProducerFactory: TaskProducerFactory {
diff --git a/Sources/SWBCore/Core.swift b/Sources/SWBCore/Core.swift
index 7686414d..8a19a5ef 100644
--- a/Sources/SWBCore/Core.swift
+++ b/Sources/SWBCore/Core.swift
@@ -590,6 +590,7 @@ public final class Core: Sendable {
 
         let specs: [SpecDump] = specRegistry.domains.flatMap { domain -> [SpecDump] in
             let allSpecs = specRegistry.findSpecs(BuildSettingsSpec.self, domain: domain, includeInherited: false)
+                + specRegistry.findSpecs(BuildSettingsExtensionSpec.self, domain: domain, includeInherited: false)
                 + specRegistry.findSpecs(BuildSystemSpec.self, domain: domain, includeInherited: false)
                 + specRegistry.findSpecs(CommandLineToolSpec.self, domain: domain, includeInherited: false)
             return allSpecs.map { spec in
diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift
index e268dcce..42fcbbe9 100644
--- a/Sources/SWBCore/Settings/Settings.swift
+++ b/Sources/SWBCore/Settings/Settings.swift
@@ -83,7 +83,7 @@ fileprivate struct PreOverridesSettings {
             let ignoredMacros: [MacroDeclaration] = [BuiltinMacros.OutputFormat, BuiltinMacros.OutputPath]
 
             for spec in specs {
-                for option in spec.flattenedBuildOptions.values {
+                for option in core.specRegistry.effectiveFlattenedBuildOptions(spec).values {
                     guard !ignoredMacros.contains(option.macro) else { continue }
 
                     if let value = option.defaultValue {
@@ -236,11 +236,11 @@ fileprivate struct PreOverridesSettings {
             // Add the defaults from all the registered tools in the given domain.
             //
             // FIXME: This is somewhat wasteful, as we end up duplicating values for super specifications. However, that lets us keep the condition set required to enable a particular compiler very simple.
-            let unionedDefaults = unionedToolDefaults(domain: domain)
+            let unionedDefaults = unionedToolDefaults(domain: domain).table
             let customizedDefaults = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace)
             for spec in core.specRegistry.findSpecs(CompilerSpec.self, domain: domain) {
                 // Add all the necessary defaults.
-                for option in spec.flattenedBuildOptions.values {
+                for option in core.specRegistry.effectiveFlattenedBuildOptions(spec).values {
                     if let defaultValue = option.defaultValue {
                         // Only push the default value if it diverges from the existing default.
                         //
diff --git a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift
index f9798b10..116a3909 100644
--- a/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift
+++ b/Sources/SWBCore/SpecImplementations/CommandLineToolSpec.swift
@@ -776,7 +776,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
         let scope = cbc.scope
         let inputFileType = cbc.inputs.first?.fileType
         let lookup = { self.lookup($0, cbc, delegate) }
-        for buildOption in self.flattenedOrderedBuildOptions {
+        for buildOption in cbc.producer.effectiveFlattenedOrderedBuildOptions(self) {
             guard let dependencyFormat = buildOption.dependencyFormat else {
                 continue
             }
@@ -910,7 +910,12 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
 
         // Add the additional outputs defined by the spec.  These are not declared as outputs but should be processed by the tool separately.
         let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate)
-        outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map({ delegate.createNode($0) }))
+        outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map { output in
+            if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
+                delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
+            }
+            return delegate.createNode(output.path)
+        })
 
         if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent {
             delegate.declareGeneratedInfoPlistContent(infoPlistContent)
@@ -972,7 +977,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
     }
 
     public struct AdditionalEvaluatedOutputsResult {
-        public var outputs = [Path]()
+        public var outputs = [(path: Path, fileType: String?)]()
         public var generatedInfoPlistContent: Path? = nil
     }
 
@@ -988,7 +993,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
 
             // FIXME: In Xcode, this is also marked as an "auxiliary output", which we use in conjunction with the "MightNotEmitAllOutput" flag to determine whether or not the tool needs to rerun if the output is missing.
 
-            result.outputs.append(output)
+            result.outputs.append((output, nil))
         }
 
         let producer = cbc.producer
@@ -996,12 +1001,17 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
         let inputFileType = cbc.inputs.first?.fileType
         let lookup = { self.lookup($0, cbc, delegate) }
         let optionContext = await discoveredCommandLineToolSpecInfo(producer, scope, delegate)
-        result.outputs.append(contentsOf: self.flattenedOrderedBuildOptions.flatMap { buildOption -> [Path] in
+        result.outputs.append(contentsOf: cbc.producer.effectiveFlattenedOrderedBuildOptions(self, filter: .all).flatMap { buildOption -> [(Path, String?)] in
             // Check if the effective arguments for this build option were non-empty as a proxy for whether it got filtered out by architecture mismatch, etc.
             guard let outputDependencies = buildOption.outputDependencies, !buildOption.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup).isEmpty else {
                 return []
             }
-            return outputDependencies.compactMap { Path(scope.evaluate($0, lookup: lookup)).nilIfEmpty?.normalize() }
+            return outputDependencies.compactMap { outputDependency in
+                guard let path = Path(scope.evaluate(outputDependency.path, lookup: lookup)).nilIfEmpty else {
+                    return nil
+                }
+                return (path.normalize(), outputDependency.fileType.map { scope.evaluate($0, lookup: lookup).nilIfEmpty } ?? nil)
+            }
         })
 
         return result
@@ -1225,7 +1235,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
                 return cbc.scope.evaluate(value, lookup: lookup).map { .literal(ByteString(encodingAsUTF8: $0)) }
 
             case .options:
-                return self.commandLineFromOptions(cbc, delegate, optionContext: optionContext, lookup: lookup)
+                return self.commandLineFromOptions(cbc, delegate, optionContext: optionContext, buildOptionsFilter: .all, lookup: lookup)
 
             case .output:
                 // We always resolve the Output via a recursive macro evaluation. See constructTasks() for more information.
@@ -1254,22 +1264,22 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
     /// Creates and returns the command line arguments generated by the options of the specification.
     ///
     /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation.
-    public func commandLineFromOptions(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileType: FileTypeSpec?, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
-        return self.flattenedOrderedBuildOptions.flatMap { $0.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) }
+    public func commandLineFromOptions(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileType: FileTypeSpec?, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
+        return producer.effectiveFlattenedOrderedBuildOptions(self, filter: buildOptionsFilter).flatMap { $0.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) }
     }
 
     /// Creates and returns the command line arguments generated by the options of the specification.
     ///
     /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation.
-    public func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
-        return commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: cbc.inputs.first?.fileType, optionContext: optionContext, lookup: { self.lookup($0, cbc, delegate, lookup) })
+    public func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter = .all, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
+        return commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: cbc.inputs.first?.fileType, optionContext: optionContext, buildOptionsFilter: buildOptionsFilter, lookup: { self.lookup($0, cbc, delegate, lookup) })
     }
 
     /// Creates and returns the command line arguments generated by the specification's build setting corresponding to the given macro declaration.
     ///
     /// - parameter lookup: An optional closure which functionally defined overriding values during build setting evaluation.
     func commandLineFromMacroDeclaration(_ producer: any CommandProducer, optionContext: (any BuildOptionGenerationContext)?, scope: MacroEvaluationScope, macro: MacroDeclaration, inputFileType: FileTypeSpec?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
-        return buildOptions.first { $0.name == macro.name }?.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) ?? []
+        return producer.effectiveBuildOptions(self).first { $0.name == macro.name }?.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup) ?? []
     }
 
     /// Creates and returns the command line arguments generated by the specification's build setting corresponding to the given macro declaration.
@@ -1285,7 +1295,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
         let scope = cbc.scope
         let inputFileType = cbc.inputs.first?.fileType
         let lookup = { self.lookup($0, cbc, delegate, lookup) }
-        return self.flattenedOrderedBuildOptions.flatMap { buildOption -> [Path] in
+        return cbc.producer.effectiveFlattenedOrderedBuildOptions(self).flatMap { buildOption -> [Path] in
             // Check if the effective arguments for this build option were non-empty as a proxy for whether it got filtered out by architecture mismatch, etc.
             guard let inputInclusions = buildOption.inputInclusions, !buildOption.getArgumentsForCommand(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, lookup: lookup).isEmpty else {
                 return []
@@ -1297,7 +1307,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
     /// Compute the list of additional linker arguments to use when this tool is used for building with the given scope.
     public func computeAdditionalLinkerArgs(_ producer: any CommandProducer, scope: MacroEvaluationScope, inputFileTypes: [FileTypeSpec], optionContext: (any BuildOptionGenerationContext)?, delegate: any TaskGenerationDelegate) async -> (args: [[String]], inputPaths: [Path]) {
         // FIXME: Optimize the list to search here.
-        return (args: self.flattenedOrderedBuildOptions.map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }, inputPaths: [])
+        return (args: producer.effectiveFlattenedOrderedBuildOptions(self).map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }, inputPaths: [])
     }
 
     // Creates and returns the environment from the specification.  This includes both the 'EnvironmentVariables' property for this tool spec, and any build options which define that their value should be exported via their 'SetValueInEnvironmentVariable' property.
@@ -1313,7 +1323,7 @@ open class CommandLineToolSpec : PropertyDomainSpec, SpecType, TaskTypeDescripti
 
         // Add environment variables from build options which specify they should be added via a 'SetValueInEnvironmentVariable' property.
         // FIXME: Optimize the list to search here.
-        for buildOption in self.flattenedOrderedBuildOptions {
+        for buildOption in cbc.producer.effectiveFlattenedOrderedBuildOptions(self) {
             if let assignment = buildOption.getEnvironmentAssignmentForCommand(cbc, lookup: wrappedLookup) {
                 environment.append(assignment)
             }
diff --git a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
index 67997f43..3ab44312 100644
--- a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
+++ b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
@@ -343,7 +343,7 @@ private let buildOptionTypes: [String: any BuildOptionType] = [
     let inputInclusions: [MacroStringExpression]?
 
     /// Additional output dependencies to consider when this build option is active.
-    let outputDependencies: [MacroStringExpression]?
+    let outputDependencies: [(path: MacroStringExpression, fileType: MacroStringExpression?)]?
 
     /// Helper function for extract an individual value definition from a dictionary.
     private static func parseBuildOptionValue(_ parser: SpecParser, _ name: String, _ type: any BuildOptionType, _ data: [String: PropertyListItem]) -> (String, BuildOptionValue)? {
@@ -804,7 +804,7 @@ private let buildOptionTypes: [String: any BuildOptionType] = [
         var dependencyFormat: DependencyDataFormat? = nil
         var featureFlags: [String]? = nil
         var inputInclusions: [MacroStringExpression]? = nil
-        var outputDependencies: [MacroStringExpression]? = nil
+        var outputDependencies: [(path: MacroStringExpression, fileType: MacroStringExpression?)]? = nil
         for (key, valueData) in items {
             switch key {
             case "Name":
@@ -1031,14 +1031,36 @@ private let buildOptionTypes: [String: any BuildOptionType] = [
             case "OutputDependencies":
                 switch valueData {
                 case .plString(let value):
-                    outputDependencies = [parser.delegate.internalMacroNamespace.parseString(value)]
+                    outputDependencies = [(parser.delegate.internalMacroNamespace.parseString(value), nil)]
                 case .plArray(let values):
                     outputDependencies = values.compactMap({ data in
-                        guard case .plString(let value) = data else {
-                            error("expected all string values in array for '\(key)'")
+                        switch data {
+                        case let .plString(value):
+                            return (parser.delegate.internalMacroNamespace.parseString(value), nil)
+                        case let .plDict(value):
+                            let path: MacroStringExpression
+                            switch value["Path"] {
+                            case let .plString(expr):
+                                path = parser.delegate.internalMacroNamespace.parseString(expr)
+                            default:
+                                error("expected string value for subkey 'Path' in element of array in '\(key)'")
+                                return nil
+                            }
+
+                            let fileType: MacroStringExpression
+                            switch value["FileType"] {
+                            case let .plString(expr):
+                                fileType = parser.delegate.internalMacroNamespace.parseString(expr)
+                            default:
+                                error("expected string value for subkey 'FileType' in element of array in '\(key)'")
+                                return nil
+                            }
+
+                            return (path, fileType)
+                        default:
+                            error("expected all string or dictionary values in array for '\(key)'")
                             return nil
                         }
-                        return parser.delegate.internalMacroNamespace.parseString(value)
                     })
                 default:
                     error("expected string or array value for build option key '\(key)'")
@@ -1720,11 +1742,11 @@ open class PropertyDomainSpec : Spec, @unchecked Sendable {
 
 
 /// Extensions to PropertyDomainSpec for performance testing.
-public extension PropertyDomainSpec {
+extension PropertyDomainSpec {
 
     /// Creates and returns a ``MacroValueAssignmentTable`` populated with the default values of the receiver's build options.
     /// The table's namespace is also returned so that the caller can add further settings to it if desired.
-    func macroTableForBuildOptionDefaults(_ core: Core) -> (MacroValueAssignmentTable, MacroNamespace) {
+    @_spi(Testing) public func macroTableForBuildOptionDefaults(_ core: Core) -> (MacroValueAssignmentTable, MacroNamespace) {
         var table = MacroValueAssignmentTable(namespace: core.specRegistry.internalMacroNamespace)
         for option in self.flattenedOrderedBuildOptions {
             guard let value = option.defaultValue else { continue }
diff --git a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
index 4bf7277f..cd18c286 100644
--- a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
+++ b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
@@ -30,6 +30,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
             ArchitectureSpec.self,
             BuildPhaseSpec.self,
             BuildSettingsSpec.self,
+            BuildSettingsExtensionSpec.self,
             BuildSystemSpec.self,
             CommandLineToolSpec.self,
             CompilerSpec.self,
diff --git a/Sources/SWBCore/SpecImplementations/Specs.swift b/Sources/SWBCore/SpecImplementations/Specs.swift
index e2b12bdc..cb8cf844 100644
--- a/Sources/SWBCore/SpecImplementations/Specs.swift
+++ b/Sources/SWBCore/SpecImplementations/Specs.swift
@@ -457,6 +457,20 @@ public final class BuildSettingsSpec : PropertyDomainSpec, SpecType, @unchecked
     }
 }
 
+public final class BuildSettingsExtensionSpec: PropertyDomainSpec, SpecType, @unchecked Sendable {
+    class public override var typeName: String {
+        return "BuildSettingsExtension"
+    }
+
+    /// The spec identifier that specs must conform to in order for these build settings to be incorporated into them.
+    let extendsConformsTo: String
+
+    public required init(_ parser: SpecParser, _ basedOnSpec: Spec?) {
+        extendsConformsTo = parser.parseRequiredString("ExtendsConformsTo")
+        super.init(parser, basedOnSpec)
+    }
+}
+
 public final class BuildSystemSpec : PropertyDomainSpec, SpecType, @unchecked Sendable {
     class public override var typeName: String {
         return "BuildSystem"
diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
index 313eba1e..a304428d 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
@@ -613,7 +613,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
         var commandLine = Array<String>()
 
         // Add the arguments from the specification.
-        commandLine += self.commandLineFromOptions(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext,lookup: { declaration in
+        commandLine += self.commandLineFromOptions(producer, scope: scope, inputFileType: inputFileType, optionContext: optionContext, buildOptionsFilter: .specOnly, lookup: { declaration in
             if declaration.name == "CLANG_INDEX_STORE_ENABLE" && optionContext is DiscoveredClangToolSpecInfo {
                 let clangToolInfo = optionContext as! DiscoveredClangToolSpecInfo
                 if !clangToolInfo.isAppleClang {
@@ -1108,6 +1108,18 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
         inputDeps.append(contentsOf: constantFlags.headerSearchPaths.inputPaths)
 #endif
 
+        let cbcWithOutput = cbc.outputs.isEmpty ? cbc.appendingOutputs([outputNode.path]) : cbc
+        commandLine += self.commandLineFromOptions(cbc.producer, scope: cbc.scope, inputFileType: resolvedInputFileType, optionContext: clangInfo, buildOptionsFilter: .extendedOnly, lookup: {
+            self.lookup($0, cbcWithOutput, delegate)
+        }).map(\.asString)
+
+        let additionalOutputs = await self.additionalEvaluatedOutputs(cbcWithOutput, delegate)
+        for output in additionalOutputs.outputs {
+            if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
+                delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
+            }
+        }
+
         // Add custom per-file flags.
         var perFileFlags = [String]()
         if let perFileArgs = input.additionalArgs {
@@ -1319,7 +1331,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
         }
 
         // Finally, create the task.
-        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode], action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
+        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + additionalOutputs.outputs.map { delegate.createNode($0.path) }, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
 
         // If the object file verifier is enabled and we are building with explicit modules, also create a job to produce adjacent objects using implicit modules, then compare the results.
         if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil {
diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
index cb78796f..f00136ed 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
@@ -402,7 +402,12 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
 
         // Add the additional outputs defined by the spec.  These are not declared as outputs but should be processed by the tool separately.
         let additionalEvaluatedOutputsResult = await additionalEvaluatedOutputs(cbc, delegate)
-        outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map({ delegate.createNode($0) }))
+        outputs.append(contentsOf: additionalEvaluatedOutputsResult.outputs.map { output in
+            if let fileTypeIdentifier = output.fileType, let fileType = cbc.producer.lookupFileType(identifier: fileTypeIdentifier) {
+                delegate.declareOutput(FileToBuild(absolutePath: output.path, fileType: fileType))
+            }
+            return delegate.createNode(output.path)
+        })
 
         if let infoPlistContent = additionalEvaluatedOutputsResult.generatedInfoPlistContent {
             delegate.declareGeneratedInfoPlistContent(infoPlistContent)
diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
index 983538cf..e1fcf101 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
@@ -2926,7 +2926,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
         var inputPaths: [Path] = []
         if !forTAPI {
             // TAPI can't use all of the additional linker options, and its spec has all of the build setting/option arguments that it can use.
-            args = self.flattenedOrderedBuildOptions.map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }.filter { !$0.isEmpty }
+            args = producer.effectiveFlattenedOrderedBuildOptions(self).map { $0.getAdditionalLinkerArgs(producer, scope: scope, inputFileTypes: inputFileTypes) }.filter { !$0.isEmpty }
         }
 
         // Determine if we are forced to use the standard system location; this is currently only for OS adopters of Swift, not any client.
diff --git a/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift b/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift
index 818147fb..48f057ac 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/TAPITools.swift
@@ -40,7 +40,7 @@ public final class TAPIToolSpec : GenericCommandLineToolSpec, GCCCompatibleCompi
     }
 
     // FIXME: Ideally this would just go in the spec itself
-    public override func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
+    public override func commandLineFromOptions(_ cbc: CommandBuildContext, _ delegate: any DiagnosticProducingDelegate, optionContext: (any BuildOptionGenerationContext)?, buildOptionsFilter: BuildOptionsFilter, lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) -> [CommandLineArgument] {
         let innerLookup: ((MacroDeclaration) -> MacroExpression?) = { macro in
             switch macro {
             case BuiltinMacros.TAPI_HEADER_SEARCH_PATHS:
@@ -67,7 +67,7 @@ public final class TAPIToolSpec : GenericCommandLineToolSpec, GCCCompatibleCompi
 
         let tapiFileListArgs: [CommandLineArgument] = ["-filelist", .path(Path(cbc.scope.evaluate(BuiltinMacros.TapiFileListPath, lookup: lookup)))]
 
-        return super.commandLineFromOptions(cbc, delegate, optionContext: optionContext, lookup: requiresSRCROOTSupport ? innerLookup : lookup) + tapiFileListArgs
+        return super.commandLineFromOptions(cbc, delegate, optionContext: optionContext, buildOptionsFilter: buildOptionsFilter, lookup: requiresSRCROOTSupport ? innerLookup : lookup) + tapiFileListArgs
     }
 
     /// Construct a new task to run the TAPI tool.
diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift
index 6d267632..c229d1f2 100644
--- a/Sources/SWBCore/TaskGeneration.swift
+++ b/Sources/SWBCore/TaskGeneration.swift
@@ -309,12 +309,67 @@ extension CommandProducer {
         return cache as! T
     }
 
+    func effectiveBuildOptions(_ spec: PropertyDomainSpec) -> [BuildOption] {
+        specRegistry.effectiveBuildOptions(spec)
+    }
+
+    func effectiveFlattenedOrderedBuildOptions(_ spec: PropertyDomainSpec, filter: BuildOptionsFilter = .all) -> [BuildOption] {
+        specRegistry.effectiveFlattenedOrderedBuildOptions(spec, filter: filter)
+    }
+
     /// Compute the expanded search path list (i.e. with recursive entries expanded) for the given macro.
     func expandedSearchPaths(for macro: PathListMacroDeclaration, scope: MacroEvaluationScope) -> [String] {
         return expandedSearchPaths(for: scope.evaluate(macro), scope: scope)
     }
 }
 
+/// Which build options to include when evaluating the command line of a spec. This primarily exists because the Clang spec wants to specially evaluate a bunch of options as "constant" (see `getStandardFlags`), which we can't guarantee to be the case for extended options.
+public enum BuildOptionsFilter {
+    /// Include only build options declared on the spec and its supertypes.
+    case specOnly
+
+    /// Include only extended build options declared via any `BuildSettingsExtension` specs.
+    case extendedOnly
+
+    /// Include build options declared by the spec itself and its supertypes, as well as via any `BuildSettingsExtension` specs.
+    case all
+}
+
+extension SpecRegistry {
+    func effectiveBuildOptions(_ spec: PropertyDomainSpec) -> [BuildOption] {
+        var options: [BuildOption] = []
+        options.append(contentsOf: spec.buildOptions)
+        for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) {
+            options.append(contentsOf: extensionSpec.buildOptions)
+        }
+        return options
+    }
+
+    func effectiveFlattenedBuildOptions(_ spec: PropertyDomainSpec) -> [String: BuildOption] {
+        var options = spec.flattenedBuildOptions
+        for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) {
+            options.merge(extensionSpec.flattenedBuildOptions, uniquingKeysWith: { _, new in
+                // Should duplicates be an error?
+                return new
+            })
+        }
+        return options
+    }
+
+    func effectiveFlattenedOrderedBuildOptions(_ spec: PropertyDomainSpec, filter: BuildOptionsFilter) -> [BuildOption] {
+        var options: [BuildOption] = []
+        if filter == .all || filter == .specOnly {
+            options.append(contentsOf: spec.flattenedOrderedBuildOptions)
+        }
+        if filter == .all || filter == .extendedOnly {
+            for extensionSpec in findSpecs(BuildSettingsExtensionSpec.self) where spec.conformsTo(identifier: extensionSpec.extendsConformsTo) {
+                options.append(contentsOf: extensionSpec.flattenedOrderedBuildOptions)
+            }
+        }
+        return options
+    }
+}
+
 /// Describes the context in which an individual invocation of a command line spec is being built.
 public struct CommandBuildContext {
     /// The settings context the command is being evaluated in.
diff --git a/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift b/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift
index aabc3b6a..3dd1ab20 100644
--- a/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift
+++ b/Sources/SWBTaskConstruction/ProductPlanning/BuildPlan.swift
@@ -172,9 +172,12 @@ package final class BuildPlan: StaleFileRemovalContext {
                 progressContinuation.yield(())
                 if delegate.cancelled { return }
 
-                // FIXME: Change this API to just return an array for now.
-                let tasks = await Array(producer.generateTasks())
-                aggregationQueue.async {
+                var tasks = await producer.generateTasks()
+                for ext in await taskProducerExtensions(planRequest.workspaceContext) {
+                    await ext.generateAdditionalTasks(&tasks, producer)
+                }
+
+                aggregationQueue.async { [tasks] in
                     productPlanResultContext.addPlannedTasks(tasks)
                 }
             }
diff --git a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift
index 7dd6ef5b..9f4da8dc 100644
--- a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift
+++ b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift
@@ -14,7 +14,7 @@ import SWBCore
 import SWBUtil
 import SWBMacro
 
-@PluginExtensionSystemActor private func taskProducerExtensions(_ workspaceContext: WorkspaceContext) -> [any TaskProducerExtension] {
+@PluginExtensionSystemActor internal func taskProducerExtensions(_ workspaceContext: WorkspaceContext) -> [any TaskProducerExtension] {
     let extensions = workspaceContext.core.pluginManager.extensions(of: TaskProducerExtensionPoint.self)
 
     // Sort the extensions using their type name so we always go through them in a stable order.
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
index 0707113e..d6cb900b 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
@@ -322,7 +322,7 @@ extension FilesBasedBuildPhaseTaskProducer {
 }
 
 /// Base class for build phase tasks producers which are based around processing file references.
-class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
+package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
     /// Captures the state used when expanding groups, to prevent recursion.
     struct GroupContext {
         /// The set of rules which have been used during this evaluation. Each individual rule is used at most once.
@@ -859,7 +859,7 @@ class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
     }
 
     /// Construct the tasks for the given rule against the supplied context.
-    func constructTasksForRule(_ rule: any BuildRuleAction, _ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
+    package func constructTasksForRule(_ rule: any BuildRuleAction, _ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) async {
         // Evaluate the rule.
         if let action = rule as? BuildRuleTaskAction {
             let spec = action.toolSpec.resolveConcreteSpec(cbc)
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
index 465be808..70c36c60 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
@@ -196,7 +196,7 @@ private final class SourcesPhaseBasedTaskGenerationDelegate: TaskGenerationDeleg
     }
 }
 
-final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBuildPhaseTaskProducer {
+package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBasedBuildPhaseTaskProducer {
     typealias ManagedBuildPhase = SourcesBuildPhase
 
     /// A virtual node representing the (conservative) construction of all non-Swift-generated headers.
@@ -312,7 +312,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
     /// The default `TaskOrderingOptions` are used for the compile tasks in the architecture-variant loop, as the default options are primarily concerned with compilation. Other tasks set up by this producer use the `nonCompilationTaskOrderingOptions` below.  But be mindful of which options are being used when adding new tasks to this producer.
     ///
     /// The rule of thumb is that using the default options imposes _more_ ordering constraints on a task.
-    override var defaultTaskOrderingOptions: TaskOrderingOptions {
+    package override var defaultTaskOrderingOptions: TaskOrderingOptions {
         let scope = self.context.settings.globalScope
 
         var options = [TaskOrderingOptions]()
@@ -340,7 +340,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
     ///
     /// We override this to auto-attach tasks to the generated headers completion ordering gate.
     @discardableResult
-    override func appendGeneratedTasks( _ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) {
+    package override func appendGeneratedTasks( _ tasks: inout [any PlannedTask], options: TaskOrderingOptions? = nil, body: (any TaskGenerationDelegate) async -> Void) async -> (tasks: [any PlannedTask], outputs: [FileToBuild]) {
         return await super.appendGeneratedTasks(&tasks, options: options) { delegate in
             await body(SourcesPhaseBasedTaskGenerationDelegate(producer: self, userPreferences: context.workspaceContext.userPreferences, delegate: delegate))
         }
@@ -700,7 +700,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
         prepareTargetForIndexInputsObjectSet.formUnion(newInputs.map{ ObjectIdentifier($0) })
     }
 
-    func prepare() {
+    package func prepare() {
         // CodeSigning in a monster... really, it is. Further, we don't actually have a model where we can perform proper pre-planning to determine what inputs will cause downstream outputs to end up in a particular location. Every source file has the potential to contribute some type of output that ends up in the wrapper.
         // For example, every metal file creates a library that is embedded into the product wrapper. However, the build system doesn't actually *know* that, as the outputs aren't really tracked in a way that the CodeSign task itself can depend on it.
         // What the build system does _know_, is that _all_ source files run tools that can potentially end up invalidating the code signature. Until we have a proper pre-planning (e.g. or dry-run) model (or a model that allows us to have dependencies on task producers), we need to be more conservative in our tracking of inputs, even if they can result in additional codesign work.
@@ -719,7 +719,7 @@ final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBase
     }
 
     /// Compute a flattened list of build files to use
-    func generateTasks() async -> [any PlannedTask] {
+    package func generateTasks() async -> [any PlannedTask] {
         // NOTE: The sources build phase uses its own generateTasks() to deal with the per-variant and per-arch nature.
         let scope = context.settings.globalScope
 
diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
index 144a07b5..aa8c902a 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
@@ -103,7 +103,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
     let provisionalTasks: [String: ProvisionalTask]
 
     /// The build rule set for file references (includes system rules as well as any custom rules).
-    let buildRuleSet: any BuildRuleSet
+    package let buildRuleSet: any BuildRuleSet
 
     /// The default working directory path to use.
     ///
diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift
index 5f09aa00..555f60f4 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducerExtensionPoint.swift
@@ -28,6 +28,8 @@ package protocol TaskProducerExtension: Sendable {
     var unorderedPostSetupTaskProducers: [any TaskProducerFactory] { get }
     var unorderedPostBuildPhasesTaskProducers: [any TaskProducerFactory] { get }
     var globalTaskProducers: [any GlobalTaskProducerFactory] { get }
+
+    func generateAdditionalTasks(_ tasks: inout [any PlannedTask], _ producer: any TaskProducer) async
 }
 
 package protocol TaskProducerFactory: Sendable {
diff --git a/Sources/SWBUniversalPlatform/Plugin.swift b/Sources/SWBUniversalPlatform/Plugin.swift
index b50a687c..e6c44e50 100644
--- a/Sources/SWBUniversalPlatform/Plugin.swift
+++ b/Sources/SWBUniversalPlatform/Plugin.swift
@@ -74,6 +74,9 @@ struct UniversalPlatformTaskProducerExtension: TaskProducerExtension {
     var unorderedPostBuildPhasesTaskProducers: [any SWBTaskConstruction.TaskProducerFactory] { [] }
 
     var globalTaskProducers: [any SWBTaskConstruction.GlobalTaskProducerFactory] { [] }
+
+    func generateAdditionalTasks(_ tasks: inout [any SWBCore.PlannedTask], _ producer: any SWBTaskConstruction.TaskProducer) {
+    }
 }
 
 struct UniversalPlatformTaskActionExtension: TaskActionExtension {
diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
index d85ccb2c..a22ca64c 100644
--- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
+++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
@@ -13,7 +13,7 @@
 import Testing
 
 import SWBTestSupport
-import SWBCore
+@_spi(Testing) import SWBCore
 import SWBUtil
 import enum SWBProtocol.ExternalToolResult
 import struct SWBProtocol.BuildOperationTaskEnded

From 85fafec9a4a2282973acf74da2c28868db3c3356 Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Fri, 1 Aug 2025 13:59:01 -0700
Subject: [PATCH 29/49] Ensure module output directories are explicitly created
 by the build system

The driver may store temporary response files in these directories, so we should create them eagerly

rdar://157302464
---
 Sources/SWBCore/Settings/BuiltinMacros.swift  |  4 +--
 .../SpecImplementations/Tools/CCompiler.swift |  2 +-
 .../Tools/SwiftCompiler.swift                 |  4 +--
 Sources/SWBCore/WorkspaceContext.swift        |  2 +-
 .../BuildDescriptionManager.swift             |  4 +--
 .../CleanOperationTests.swift                 |  8 +++---
 .../PackageBuildOperationTests.swift          |  4 +++
 Tests/SWBCoreTests/SettingsTests.swift        |  2 +-
 .../TaskConstructionTests.swift               | 26 +++++++++++++++++++
 9 files changed, 43 insertions(+), 13 deletions(-)

diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift
index bef7fc81..4fe82fc0 100644
--- a/Sources/SWBCore/Settings/BuiltinMacros.swift
+++ b/Sources/SWBCore/Settings/BuiltinMacros.swift
@@ -506,8 +506,8 @@ public final class BuiltinMacros {
     public static let CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER = BuiltinMacros.declareBooleanMacro("CLANG_ENABLE_EXPLICIT_MODULES_WITH_COMPILER_LAUNCHER")
     public static let CLANG_EXPLICIT_MODULES_LIBCLANG_PATH = BuiltinMacros.declareStringMacro("CLANG_EXPLICIT_MODULES_LIBCLANG_PATH")
     public static let CLANG_EXPLICIT_MODULES_IGNORE_LIBCLANG_VERSION_MISMATCH = BuiltinMacros.declareBooleanMacro("CLANG_EXPLICIT_MODULES_IGNORE_LIBCLANG_VERSION_MISMATCH")
-    public static let CLANG_EXPLICIT_MODULES_OUTPUT_PATH = BuiltinMacros.declareStringMacro("CLANG_EXPLICIT_MODULES_OUTPUT_PATH")
-    public static let SWIFT_EXPLICIT_MODULES_OUTPUT_PATH = BuiltinMacros.declareStringMacro("SWIFT_EXPLICIT_MODULES_OUTPUT_PATH")
+    public static let CLANG_EXPLICIT_MODULES_OUTPUT_PATH = BuiltinMacros.declarePathMacro("CLANG_EXPLICIT_MODULES_OUTPUT_PATH")
+    public static let SWIFT_EXPLICIT_MODULES_OUTPUT_PATH = BuiltinMacros.declarePathMacro("SWIFT_EXPLICIT_MODULES_OUTPUT_PATH")
     public static let CLANG_ENABLE_COMPILE_CACHE = BuiltinMacros.declareBooleanMacro("CLANG_ENABLE_COMPILE_CACHE")
     public static let CLANG_CACHE_FINE_GRAINED_OUTPUTS = BuiltinMacros.declareEnumMacro("CLANG_CACHE_FINE_GRAINED_OUTPUTS") as EnumMacroDeclaration<FineGrainedCachingSetting>
     public static let CLANG_CACHE_FINE_GRAINED_OUTPUTS_VERIFICATION = BuiltinMacros.declareEnumMacro("CLANG_CACHE_FINE_GRAINED_OUTPUTS_VERIFICATION") as EnumMacroDeclaration<FineGrainedCachingVerificationSetting>
diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
index 313eba1e..d21e3cb6 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
@@ -977,7 +977,7 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
                     usesCompilerLauncher: usesCompilerLauncher,
                     // This path is scoped to the project, so ideally different targets that use the same modules would
                     // share precompiled modules.
-                    outputPath: Path(cbc.scope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH)),
+                    outputPath: cbc.scope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH),
                     scanningOutputPath: scanningOutputPath,
                     casOptions: casOptions,
                     cacheFallbackIfNotAvailable: cbc.scope.evaluate(BuiltinMacros.CLANG_CACHE_FALLBACK_IF_UNAVAILABLE),
diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
index 983538cf..b9b05a4d 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
@@ -1809,7 +1809,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                 // Instructs the driver to perform build planning with explicit module builds
                 if explicitModuleBuildEnabled {
                     args.append("-explicit-module-build")
-                    let explicitDependencyOutputPath = Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH))
+                    let explicitDependencyOutputPath = cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)
                     args.append(contentsOf: ["-module-cache-path", explicitDependencyOutputPath.str])
                     let moduleCacheDir = cbc.scope.evaluate(BuiltinMacros.MODULE_CACHE_DIR)
                     if LibSwiftDriver.supportsDriverFlag(spelled: "-clang-scanner-module-cache-path"),
@@ -2296,7 +2296,7 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                 previewPayload: previewPayload,
                 localizationPayload: localizationPayload,
                 numExpectedCompileSubtasks: isUsingWholeModuleOptimization ? 1 : cbc.inputs.count,
-                driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: Path(cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath), previewStyle: cbc.scope.previewStyle
+                driverPayload: await driverPayload(uniqueID: String(args.hashValue), scope: cbc.scope, delegate: delegate, compilationMode: compilationMode, isUsingWholeModuleOptimization: isUsingWholeModuleOptimization, args: args, tempDirPath: objectFileDir, explicitModulesTempDirPath: cbc.scope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH), variant: variant, arch: arch + compilationMode.moduleBaseNameSuffix, commandLine: ["builtin-SwiftDriver", "--"] + args, ruleInfo: ruleInfo(compilationMode.ruleNameIntegratedDriver, targetName), casOptions: casOptions, linkerResponseFilePath: moduleLinkerArgsPath), previewStyle: cbc.scope.previewStyle
             )
 
             // Finally, assemble the input and output paths and create the Swift compiler command.
diff --git a/Sources/SWBCore/WorkspaceContext.swift b/Sources/SWBCore/WorkspaceContext.swift
index 1b61e537..21da1876 100644
--- a/Sources/SWBCore/WorkspaceContext.swift
+++ b/Sources/SWBCore/WorkspaceContext.swift
@@ -352,7 +352,7 @@ public final class WorkspaceContext: Sendable {
     private let headerIndexCache = AsyncSingleValueCache<WorkspaceHeaderIndex, Never>()
 
     public var buildDirectoryMacros: [PathMacroDeclaration] {
-        return [BuiltinMacros.DSTROOT, BuiltinMacros.OBJROOT, BuiltinMacros.SYMROOT, BuiltinMacros.BUILT_PRODUCTS_DIR, BuiltinMacros.EAGER_LINKING_INTERMEDIATE_TBD_DIR]
+        return [BuiltinMacros.DSTROOT, BuiltinMacros.OBJROOT, BuiltinMacros.SYMROOT, BuiltinMacros.BUILT_PRODUCTS_DIR, BuiltinMacros.EAGER_LINKING_INTERMEDIATE_TBD_DIR, BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH, BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH]
     }
 
     /// The path to the module session file for the workspace given a set of build parameters.
diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
index bc1da89a..7fcf1ebd 100644
--- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift
+++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
@@ -198,8 +198,8 @@ package final class BuildDescriptionManager: Sendable {
 
             moduleCachePathsPerTarget[target] = [
                 settings.globalScope.evaluate(BuiltinMacros.MODULE_CACHE_DIR),
-                Path(settings.globalScope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH)),
-                Path(settings.globalScope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH)),
+                settings.globalScope.evaluate(BuiltinMacros.SWIFT_EXPLICIT_MODULES_OUTPUT_PATH),
+                settings.globalScope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH),
             ]
 
             if shouldValidateCAS, settings.globalScope.evaluate(BuiltinMacros.CLANG_ENABLE_COMPILE_CACHE) || settings.globalScope.evaluate(BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE) {
diff --git a/Tests/SWBBuildSystemTests/CleanOperationTests.swift b/Tests/SWBBuildSystemTests/CleanOperationTests.swift
index 549d1d5c..e81dc4a7 100644
--- a/Tests/SWBBuildSystemTests/CleanOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/CleanOperationTests.swift
@@ -106,7 +106,7 @@ fileprivate struct CleanOperationTests: CoreBasedTests {
     @Test(.requireSDKs(.macOS))
     func cleanFramework() async throws {
         try await withTestHarness { tester, tmpDirPath, _ in
-            let buildFolderPaths = [ tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug")]
+            let buildFolderPaths = [ tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug"), tmpDirPath.join("Test/aProject/build/ExplicitPrecompiledModules"), tmpDirPath.join("Test/aProject/build/SwiftExplicitPrecompiledModules")]
 
             try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in
                 // Check if build folder tasks have run as expected.
@@ -136,7 +136,7 @@ fileprivate struct CleanOperationTests: CoreBasedTests {
     @Test(.requireSDKs(.macOS))
     func cleanFrameworkInstall() async throws {
         try await withTestHarness(install: true) { tester, tmpDirPath, dstRoot in
-            let buildFolderPaths = [ dstRoot, tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug")]
+            let buildFolderPaths = [ dstRoot, tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug"), tmpDirPath.join("Test/aProject/build/ExplicitPrecompiledModules"), tmpDirPath.join("Test/aProject/build/SwiftExplicitPrecompiledModules")]
 
             try await tester.checkBuild(runDestination: .macOS, persistent: true) { results in
                 // Check if build folder tasks have run as expected.
@@ -212,7 +212,7 @@ fileprivate struct CleanOperationTests: CoreBasedTests {
             let buildRequest = BuildRequest(parameters: parameters, buildTargets: buildTargets, continueBuildingAfterErrors: true, useParallelTargets: true, useImplicitDependencies: false, useDryRun: false)
             try await tester.checkBuild(runDestination: .macOS, buildRequest: buildRequest, persistent: true) { results in
                 results.checkTasks(.matchRuleType("CreateBuildDirectory")) { tasks in
-                    #expect(tasks.count == 6)
+                    #expect(tasks.count == 10)
                 }
 
                 results.checkNoTask(.matchRuleType("CreateBuildDirectory"))
@@ -234,7 +234,7 @@ fileprivate struct CleanOperationTests: CoreBasedTests {
     @Test(.requireSDKs(.macOS))
     func cleanDoesNotDeleteManuallyCreatedFolders() async throws {
         try await withTestHarness { tester, tmpDirPath, _ in
-            let buildFolderPaths = [ tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug")]
+            let buildFolderPaths = [ tmpDirPath.join("Test/aProject/build"), tmpDirPath.join("Test/aProject/build/Debug"), tmpDirPath.join("Test/aProject/build/EagerLinkingTBDs/Debug"), tmpDirPath.join("Test/aProject/build/ExplicitPrecompiledModules"), tmpDirPath.join("Test/aProject/build/SwiftExplicitPrecompiledModules")]
 
             for folder in buildFolderPaths {
                 try tester.fs.createDirectory(folder, recursive: true)
diff --git a/Tests/SWBBuildSystemTests/PackageBuildOperationTests.swift b/Tests/SWBBuildSystemTests/PackageBuildOperationTests.swift
index d854664c..6563b003 100644
--- a/Tests/SWBBuildSystemTests/PackageBuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/PackageBuildOperationTests.swift
@@ -592,11 +592,15 @@ fileprivate struct PackageBuildOperationTests: CoreBasedTests {
                 packageBuildDirectory.join(configurationToBuild).str,
                 packageBuildDirectory.join(configurationToBuild).join("PackageFrameworks").str,
                 packageBuildDirectory.join("EagerLinkingTBDs").join(configurationToBuild).str,
+                packageBuildDirectory.join("ExplicitPrecompiledModules").str,
+                packageBuildDirectory.join("SwiftExplicitPrecompiledModules").str,
 
                 projectBuildDirectory.str,
                 projectBuildDirectory.join(configurationToBuild).str,
                 projectBuildDirectory.join(configurationToBuild).join("PackageFrameworks").str,
                 projectBuildDirectory.join("EagerLinkingTBDs").join(configurationToBuild).str,
+                projectBuildDirectory.join("ExplicitPrecompiledModules").str,
+                projectBuildDirectory.join("SwiftExplicitPrecompiledModules").str,
             ].sorted()
 
             let tester = try await testerForBasicPackageProject(tmpDirPath: tmpDirPath, configurationToBuild: configurationToBuild)
diff --git a/Tests/SWBCoreTests/SettingsTests.swift b/Tests/SWBCoreTests/SettingsTests.swift
index 47f6cf6a..fc9f0ef0 100644
--- a/Tests/SWBCoreTests/SettingsTests.swift
+++ b/Tests/SWBCoreTests/SettingsTests.swift
@@ -396,7 +396,7 @@ import SWBMacro
                 if let project = settings.project {
                     #expect(settings.globalScope.evaluate(BuiltinMacros.OBJROOT) == Path("\(project.sourceRoot.str)/build"))
                     #expect(settings.globalScope.evaluate(BuiltinMacros.CONFIGURATION_BUILD_DIR) == Path("\(project.sourceRoot.str)/build/Config1"))
-                    #expect(settings.globalScope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH) == "\(project.sourceRoot.str)/build/ExplicitPrecompiledModules")
+                    #expect(settings.globalScope.evaluate(BuiltinMacros.CLANG_EXPLICIT_MODULES_OUTPUT_PATH).str == "\(project.sourceRoot.str)/build/ExplicitPrecompiledModules")
                 }
 
                 // check that we get the right value for VERSION_INFO_STRING, which validates we parsed it correctly.
diff --git a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
index ea146fcd..4f9a3e50 100644
--- a/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/TaskConstructionTests.swift
@@ -2136,6 +2136,8 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                             .namePattern(.prefix("CreateBuildDirectory-/tmp/Test/aProject/build")),
                             .namePattern(.prefix("CreateBuildDirectory-/tmp/Test/aProject/build/Release\(runDestination.builtProductsDirSuffix)/BuiltProducts")),
                             .namePattern(.prefix("CreateBuildDirectory-/tmp/Test/aProject/build/EagerLinkingTBDs")),
+                            .namePattern(.prefix("CreateBuildDirectory-/tmp/Test/aProject/build/SwiftExplicitPrecompiledModules")),
+                            .namePattern(.prefix("CreateBuildDirectory-/tmp/Test/aProject/build/ExplicitPrecompiledModules")),
                             .namePattern(.and(.prefix("target"), .suffix("-begin-compiling"))),
                         ])
                     } else {
@@ -8260,6 +8262,30 @@ fileprivate struct TaskConstructionTests: CoreBasedTests {
                     task.checkInputs([.path("\(tmpDir.str)/build/a/b/c/d")])
                 }
 
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/b/c/d/ExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a/b/c/d")])
+                }
+
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/b/c/d/SwiftExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a/b/c/d")])
+                }
+
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/b/ExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a/b")])
+                }
+
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/b/SwiftExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a/b")])
+                }
+
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/ExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a")])
+                }
+
+                results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/SwiftExplicitPrecompiledModules")) { task in
+                    task.checkInputs([.path("\(tmpDir.str)/build/a")])
+                }
+
                 results.checkTask(.matchRuleType("CreateBuildDirectory"), .matchRuleItem("\(tmpDir.str)/build/a/b/c/d")) { task in
                     task.checkInputs([.path("\(tmpDir.str)/build/a/b")])
                 }

From db3a4e3c2fd7951c88e659eee7496948aaca5f38 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20B=C3=BCgling?= <bbuegling@apple.com>
Date: Mon, 4 Aug 2025 10:56:49 -0700
Subject: [PATCH 30/49] Handle cross-language duplications for missing
 dependencies (#696)

Instead of emitting diagnostics separately per language, this emits them together by first computing missing dependencies for each language and then merging/deduplicating them before diagnostic generation.

The code here will be forward compatible when we extend the Clang implementation to respect access levels and/or provide import locations.
---
 Sources/SWBCore/Dependencies.swift            | 71 +++++++--------
 .../ValidateDependenciesTaskAction.swift      | 36 ++++++--
 Sources/SWBTestSupport/CoreBasedTests.swift   |  8 ++
 .../DependencyValidationTests.swift           | 91 +++++++++++++++++++
 4 files changed, 163 insertions(+), 43 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index a05f33e3..0675792a 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -17,7 +17,7 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
     public let name: String
     public let accessLevel: AccessLevel
 
-    public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable {
+    public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable, Comparable {
         case Private = "private"
         case Package = "package"
         case Public = "public"
@@ -29,6 +29,18 @@ public struct ModuleDependency: Hashable, Sendable, SerializableCodable {
 
             self = accessLevel
         }
+
+        // This allows easy merging of different access levels to always end up with the broadest one needed for a target.
+        public static func < (lhs: Self, rhs: Self) -> Bool {
+            switch lhs {
+            case .Private:
+                return true
+            case .Public:
+                return false
+            case .Package:
+                return rhs == .Public
+            }
+        }
     }
 
     public init(name: String, accessLevel: AccessLevel) {
@@ -80,18 +92,15 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
         self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
     }
 
-    /// Make diagnostics for missing module dependencies from Clang imports.
+    /// Compute missing module dependencies from Clang imports.
     ///
     /// The compiler tracing information does not provide the import locations or whether they are public imports
     /// (which depends on whether the import is in an installed header file).
     /// If `files` is nil, the current toolchain does support the feature to trace imports.
-    public func makeDiagnostics(files: [Path]?) -> [Diagnostic] {
+    public func computeMissingDependencies(files: [Path]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
         guard validate != .no else { return [] }
         guard let files else {
-            return [Diagnostic(
-                behavior: .warning,
-                location: .unknown,
-                data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
+            return nil
         }
 
         // The following is a provisional/incomplete mechanism for resolving a module dependency from a file path.
@@ -111,39 +120,19 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
             ModuleDependency(name: $0, accessLevel: .Private)
         })
 
-        guard !missingDeps.isEmpty else { return [] }
-
-        let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
-
-        let fixIt = fixItContext?.makeFixIt(newModules: Array(missingDeps))
-        let fixIts = fixIt.map { [$0] } ?? []
-
-        let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
-
-        let location: Diagnostic.Location = fixIt.map {
-            Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
-        } ?? Diagnostic.Location.buildSetting(BuiltinMacros.MODULE_DEPENDENCIES)
-
-        return [Diagnostic(
-            behavior: behavior,
-            location: location,
-            data: DiagnosticData(message),
-            fixIts: fixIts)]
+        return missingDeps.map { ($0, []) }
     }
 
-    /// Make diagnostics for missing module dependencies from Swift imports.
+    /// Compute missing module dependencies from Swift imports.
     ///
     /// If `imports` is nil, the current toolchain does not support the features to gather imports.
-    public func makeDiagnostics(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
+    public func computeMissingDependencies(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
         guard validate != .no else { return [] }
         guard let imports else {
-            return [Diagnostic(
-                behavior: .warning,
-                location: .unknown,
-                data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
+            return nil
         }
 
-        let missingDeps = imports.filter {
+        return imports.filter {
             // ignore module deps without source locations, these are inserted by swift / swift-build and we should treat them as implementation details which we can track without needing the user to declare them
             if $0.importLocations.isEmpty { return false }
 
@@ -151,15 +140,25 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
             if moduleDependencies.contains($0.0) { return false }
             return true
         }
+    }
+
+    /// Make diagnostics for missing module dependencies.
+    public func makeDiagnostics(missingDependencies: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [Diagnostic] {
+        guard let missingDependencies else {
+            return [Diagnostic(
+                behavior: .warning,
+                location: .unknown,
+                data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_MODULE_DEPENDENCIES.name)"))]
+        }
 
-        guard !missingDeps.isEmpty else { return [] }
+        guard !missingDependencies.isEmpty else { return [] }
 
         let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
 
-        let fixIt = fixItContext?.makeFixIt(newModules: missingDeps.map { $0.0 })
+        let fixIt = fixItContext?.makeFixIt(newModules: missingDependencies.map { $0.0 })
         let fixIts = fixIt.map { [$0] } ?? []
 
-        let importDiags: [Diagnostic] = missingDeps
+        let importDiags: [Diagnostic] = missingDependencies
             .flatMap { dep in
                 dep.1.map {
                     return Diagnostic(
@@ -170,7 +169,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
                 }
             }
 
-        let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDeps.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
+        let message = "Missing entries in \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(missingDependencies.map { $0.0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
 
         let location: Diagnostic.Location = fixIt.map {
             Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
index 313262ab..06f67771 100644
--- a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
@@ -36,8 +36,8 @@ public final class ValidateDependenciesTaskAction: TaskAction {
         }
 
         do {
-            var allFiles = Set<String>()
-            var allImports = Set<DependencyValidationInfo.Import>()
+            var allClangFiles = Set<String>()
+            var allSwiftImports = Set<DependencyValidationInfo.Import>()
             var unsupported = false
 
             for inputPath in task.inputPaths {
@@ -47,11 +47,11 @@ public final class ValidateDependenciesTaskAction: TaskAction {
                 switch info.payload {
                 case .clangDependencies(let files):
                     files.forEach {
-                        allFiles.insert($0)
+                        allClangFiles.insert($0)
                     }
                 case .swiftDependencies(let imports):
                     imports.forEach {
-                        allImports.insert($0)
+                        allSwiftImports.insert($0)
                     }
                 case .unsupported:
                     unsupported = true
@@ -61,10 +61,32 @@ public final class ValidateDependenciesTaskAction: TaskAction {
             var diagnostics: [Diagnostic] = []
 
             if unsupported {
-                diagnostics.append(contentsOf: context.makeDiagnostics(files: nil))
+                diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: nil))
             } else {
-                diagnostics.append(contentsOf: context.makeDiagnostics(files: allFiles.map { Path($0) }))
-                diagnostics.append(contentsOf: context.makeDiagnostics(imports: allImports.map { ($0.dependency, $0.importLocations) }))
+                let clangMissingDeps = context.computeMissingDependencies(files: allClangFiles.map { Path($0) })
+                let swiftMissingDeps = context.computeMissingDependencies(imports: allSwiftImports.map { ($0.dependency, $0.importLocations) })
+
+                // Update Swift dependencies with information from Clang dependencies on the same module.
+                var clangMissingDepsByName = [String: (ModuleDependency, importLocations: [Diagnostic.Location])]()
+                clangMissingDeps?.forEach {
+                    clangMissingDepsByName[$0.0.name] = $0
+                }
+                let updatedSwiftMissingDeps: [(ModuleDependency, importLocations: [Diagnostic.Location])] = swiftMissingDeps?.map {
+                    if let clangMissingDep = clangMissingDepsByName[$0.0.name] {
+                        return (
+                            ModuleDependency(name: $0.0.name, accessLevel: max($0.0.accessLevel, clangMissingDep.0.accessLevel)),
+                            $0.importLocations + clangMissingDep.importLocations
+                        )
+                    } else {
+                        return $0
+                    }
+                } ?? []
+
+                // Filter missing C dependencies by known Swift dependencies to avoid duplicate diagnostics.
+                let swiftImports = Set(allSwiftImports.map { $0.dependency.name })
+                let uniqueClangMissingDeps = clangMissingDeps?.filter { !swiftImports.contains($0.0.name) } ?? []
+
+                diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: uniqueClangMissingDeps + updatedSwiftMissingDeps))
             }
 
             for diagnostic in diagnostics {
diff --git a/Sources/SWBTestSupport/CoreBasedTests.swift b/Sources/SWBTestSupport/CoreBasedTests.swift
index b6b0e837..1ac926fd 100644
--- a/Sources/SWBTestSupport/CoreBasedTests.swift
+++ b/Sources/SWBTestSupport/CoreBasedTests.swift
@@ -155,6 +155,14 @@ extension CoreBasedTests {
         }
     }
 
+    /// The path to clang in the default toolchain.
+    public var defaultClangPath: Path {
+        get async throws {
+            let clangInfo = try await clangInfo
+            return clangInfo.toolPath
+        }
+    }
+
     /// The path to the libClang.dylib compiler in the default toolchain.
     package var libClangPath: Path {
         get async throws {
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 8c156951..3fd4b2cd 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -19,6 +19,23 @@ import SWBMacro
 
 @Suite
 fileprivate struct DependencyValidationTests: CoreBasedTests {
+    @Test
+    func moduleDependencyAccessLevelComparable() async throws {
+        #expect(ModuleDependency.AccessLevel.Private == .Private)
+        #expect(ModuleDependency.AccessLevel.Private < .Package)
+        #expect(ModuleDependency.AccessLevel.Private < .Public)
+
+        #expect(ModuleDependency.AccessLevel.Package == .Package)
+        #expect(ModuleDependency.AccessLevel.Package < .Public)
+        #expect(ModuleDependency.AccessLevel.Package > .Private)
+
+        #expect(ModuleDependency.AccessLevel.Public == .Public)
+        #expect(ModuleDependency.AccessLevel.Public > .Private)
+        #expect(ModuleDependency.AccessLevel.Public > .Package)
+
+        #expect(max(ModuleDependency.AccessLevel.Public, ModuleDependency.AccessLevel.Private) == .Public)
+    }
+
     @Test(.requireSDKs(.macOS))
     func dependencyValidation() async throws {
         try await testDependencyValidation(BuildParameters(configuration: "Debug"))
@@ -529,4 +546,78 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.host), .requireClangFeatures(.printHeadersDirectPerFile), .skipHostOS(.windows, "toolchain too old"), .skipHostOS(.linux, "toolchain too old"))
+    func validateModuleDependenciesMixedSource() async throws {
+        try await withTemporaryDirectory { tmpDir async throws -> Void in
+            let testWorkspace = try await TestWorkspace(
+                "Test",
+                sourceRoot: tmpDir.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources", path: "Sources",
+                            children: [
+                                TestFile("CoreFoo.m"),
+                                TestFile("Swift.swift"),
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "CLANG_ENABLE_MODULES": "YES",
+                                    "CLANG_ENABLE_EXPLICIT_MODULES": "YES",
+                                    "SWIFT_ENABLE_EXPLICIT_MODULES": "YES",
+                                    "SWIFT_UPCOMING_FEATURE_INTERNAL_IMPORTS_BY_DEFAULT": "YES",
+                                    "SWIFT_VERSION": swiftVersion,
+                                    "GENERATE_INFOPLIST_FILE": "YES",
+                                    "VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR",
+                                    "SDKROOT": "$(HOST_PLATFORM)",
+                                    "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
+                                    "DSTROOT": tmpDir.join("dstroot").str,
+
+                                    // Temporarily override to use the latest toolchain in CI because we depend on swift and swift-driver changes which aren't in the baseline tools yet
+                                    "TOOLCHAINS": "swift",
+                                    // We still want to use the default clang since that is used to gate the test
+                                    "CC": defaultClangPath.str,
+                                ]
+                            )
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "CoreFoo", type: .framework,
+                                buildPhases: [
+                                    TestSourcesBuildPhase(["CoreFoo.m", "Swift.swift"]),
+                                    TestFrameworksBuildPhase()
+                                ])
+                        ])
+                ]
+            )
+
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+            let SRCROOT = testWorkspace.sourceRoot.join("aProject")
+
+            try await tester.fs.writeFileContents(SRCROOT.join("Sources/Swift.swift")) { stream in
+                stream <<< """
+                    import Foundation
+                    import AppKit
+                """
+            }
+
+            try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.m")) { contents in
+                contents <<< """
+                    #include <Foundation/Foundation.h>
+                    #include <Accelerate/Accelerate.h>
+
+                    void f(void) { };
+                """
+            }
+
+            try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
+                results.checkError(.contains("Missing entries in MODULE_DEPENDENCIES: Accelerate AppKit Foundation"))
+            }
+        }
+    }
+
 }

From 25dd8498e46719132bcc2f681229fed7a1ae4382 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 1 Aug 2025 18:26:46 -0700
Subject: [PATCH 31/49] Improve Android SDK/NDK discovery

Now respects the environment variable overrides and looks for the Debian/Ubuntu package location.

Closes #495
---
 .github/scripts/linux_pre_build.sh            |  66 +++++++
 .github/workflows/pull_request.yml            |  36 +---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   | 187 ++++++++++++++----
 Sources/SWBAndroidPlatform/CMakeLists.txt     |   1 +
 .../SWBAndroidPlatform/JavaProperties.swift   |  32 +++
 Sources/SWBAndroidPlatform/Plugin.swift       |  44 +++--
 Sources/SWBUtil/FSProxy.swift                 |  10 +-
 Sources/SWBUtil/Path.swift                    |   4 +
 Sources/SWBUtil/URL.swift                     |  12 +-
 .../AndroidSDKTests.swift                     | 111 ++++++++---
 .../SWBAndroidPlatformTests.swift             |   7 +
 11 files changed, 392 insertions(+), 118 deletions(-)
 create mode 100755 .github/scripts/linux_pre_build.sh
 create mode 100644 Sources/SWBAndroidPlatform/JavaProperties.swift

diff --git a/.github/scripts/linux_pre_build.sh b/.github/scripts/linux_pre_build.sh
new file mode 100755
index 00000000..8adf2410
--- /dev/null
+++ b/.github/scripts/linux_pre_build.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the Swift open source project
+##
+## Copyright (c) 2025 Apple Inc. and the Swift project authors
+## Licensed under Apache License v2.0 with Runtime Library Exception
+##
+## See http://swift.org/LICENSE.txt for license information
+## See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+##
+##===----------------------------------------------------------------------===##
+
+set -e
+
+if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
+    export DEBIAN_FRONTEND=noninteractive
+
+    apt-get update -y
+
+    # Build dependencies
+    apt-get install -y libsqlite3-dev libncurses-dev
+
+    # Debug symbols
+    apt-get install -y libc6-dbg
+
+    if [[ "$INSTALL_CMAKE" == "1" ]] ; then
+        apt-get install -y cmake ninja-build
+    fi
+
+    # Android NDK
+    dpkg_architecture="$(dpkg --print-architecture)"
+    if [[ "$SKIP_ANDROID" != "1" ]] && [[ "$dpkg_architecture" == amd64 ]] ; then
+        eval "$(cat /etc/lsb-release)"
+        case "$DISTRIB_CODENAME" in
+            bookworm|jammy)
+                : # Not available
+                ;;
+            noble)
+                apt-get install -y google-android-ndk-r26c-installer
+                ;;
+            *)
+                echo "Unknown distribution: $DISTRIB_CODENAME" >&2
+                exit 1
+        esac
+    else
+        echo "Skipping Android NDK installation on $dpkg_architecture" >&2
+    fi
+elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
+    dnf update -y
+
+    # Build dependencies
+    dnf install -y sqlite-devel ncurses-devel
+
+    # Debug symbols
+    dnf debuginfo-install -y glibc
+elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
+    yum update -y
+
+    # Build dependencies
+    yum install -y sqlite-devel ncurses-devel
+
+    # Debug symbols
+    yum install -y yum-utils
+    debuginfo-install -y glibc
+fi
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index c39112b4..1f1e70d1 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -14,33 +14,7 @@ jobs:
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
     with:
       linux_os_versions: '["noble", "jammy", "rhel-ubi9"]'
-      linux_pre_build_command: |
-        if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
-          apt-get update -y
-
-          # Build dependencies
-          apt-get install -y libsqlite3-dev libncurses-dev
-
-          # Debug symbols
-          apt-get install -y libc6-dbg
-        elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
-          dnf update -y
-
-          # Build dependencies
-          dnf install -y sqlite-devel ncurses-devel
-
-          # Debug symbols
-          dnf debuginfo-install -y glibc
-        elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
-          yum update -y
-
-          # Build dependencies
-          yum install -y sqlite-devel ncurses-devel
-
-          # Debug symbols
-          yum install -y yum-utils
-          debuginfo-install -y glibc
-        fi
+      linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
       linux_build_command: 'swift test --no-parallel'
       linux_swift_versions: '["nightly-main", "nightly-6.2"]'
       windows_swift_versions: '["nightly-main"]'
@@ -50,13 +24,7 @@ jobs:
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
     with:
       linux_os_versions: '["noble"]'
-      linux_pre_build_command: |
-        apt-get update -y
-
-        # Build dependencies
-        apt-get install -y libsqlite3-dev libncurses-dev
-
-        apt-get install -y cmake ninja-build
+      linux_pre_build_command: SKIP_ANDROID=1 INSTALL_CMAKE=1 ./.github/scripts/linux_pre_build.sh
       linux_build_command: 'swift package -Xbuild-tools-swiftc -DUSE_PROCESS_SPAWNING_WORKAROUND cmake-smoke-test --disable-sandbox --cmake-path `which cmake` --ninja-path `which ninja` --extra-cmake-arg -DCMAKE_C_COMPILER=`which clang` --extra-cmake-arg -DCMAKE_CXX_COMPILER=`which clang++` --extra-cmake-arg -DCMAKE_Swift_COMPILER=`which swiftc`'
       linux_swift_versions: '["nightly-main"]'
       windows_swift_versions: '[]'
diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index cb66619d..3b934838 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -15,43 +15,54 @@ public import Foundation
 
 @_spi(Testing) public struct AndroidSDK: Sendable {
     public let host: OperatingSystem
-    public let path: Path
+    public let path: AbsolutePath
+    private let ndkInstallations: NDK.Installations
 
     /// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
-    @_spi(Testing) public let ndks: [NDK]
+    @_spi(Testing) public var ndks: [NDK] {
+        ndkInstallations.ndks
+    }
 
-    public var latestNDK: NDK? {
-        ndks.last
+    public var preferredNDK: NDK? {
+        ndkInstallations.preferredNDK ?? ndks.last
     }
 
-    init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
+    init(host: OperatingSystem, path: AbsolutePath, fs: any FSProxy) throws {
         self.host = host
         self.path = path
-        self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
+        self.ndkInstallations = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
     }
 
     @_spi(Testing) public struct NDK: Equatable, Sendable {
         public static let minimumNDKVersion = Version(23)
 
         public let host: OperatingSystem
-        public let path: Path
+        public let path: AbsolutePath
         public let version: Version
         public let abis: [String: ABI]
         public let deploymentTargetRange: DeploymentTargetRange
 
-        init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
+        @_spi(Testing) public init(host: OperatingSystem, path ndkPath: AbsolutePath, fs: any FSProxy) throws {
             self.host = host
             self.path = ndkPath
-            self.version = version
+            self.toolchainPath = try AbsolutePath(validating: path.path.join("toolchains").join("llvm").join("prebuilt").join(Self.hostTag(host)))
+            self.sysroot = try AbsolutePath(validating: toolchainPath.path.join("sysroot"))
+
+            let propertiesFile = ndkPath.path.join("source.properties")
+            guard fs.exists(propertiesFile) else {
+                throw Error.notAnNDK(ndkPath)
+            }
 
-            let metaPath = ndkPath.join("meta")
+            self.version = try NDK.Properties(data: Data(fs.read(propertiesFile))).revision
+
+            let metaPath = ndkPath.path.join("meta")
 
             guard #available(macOS 14, *) else {
                 throw StubError.error("Unsupported macOS version")
             }
 
             if version < Self.minimumNDKVersion {
-                throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
+                throw Error.unsupportedVersion(path: ndkPath, minimumVersion: Self.minimumNDKVersion)
             }
 
             self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis
@@ -65,6 +76,36 @@ public import Foundation
             deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
         }
 
+        public enum Error: Swift.Error, CustomStringConvertible, Sendable {
+            case notAnNDK(AbsolutePath)
+            case unsupportedVersion(path: AbsolutePath, minimumVersion: Version)
+            case noSupportedVersions(minimumVersion: Version)
+
+            public var description: String {
+                switch self {
+                case let .notAnNDK(path):
+                    "Package at path '\(path.path.str)' is not an Android NDK (no source.properties file)"
+                case let .unsupportedVersion(path, minimumVersion):
+                    "Android NDK version at path '\(path.path.str)' is not supported (r\(minimumVersion.description) or later required)"
+                case let .noSupportedVersions(minimumVersion):
+                    "All installed NDK versions are not supported (r\(minimumVersion.description) or later required)"
+                }
+            }
+        }
+
+        struct Properties {
+            let properties: JavaProperties
+            let revision: Version
+
+            init(data: Data) throws {
+                properties = try .init(data: data)
+                guard properties["Pkg.Desc"] == "Android NDK" else {
+                    throw StubError.error("Package is not an Android NDK")
+                }
+                revision = try Version(properties["Pkg.BaseRevision"] ?? properties["Pkg.Revision"] ?? "")
+            }
+        }
+
         struct ABIs: DecodableWithConfiguration {
             let abis: [String: ABI]
 
@@ -161,15 +202,10 @@ public import Foundation
             public let max: Int
         }
 
-        public var toolchainPath: Path {
-            path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
-        }
-
-        public var sysroot: Path {
-            toolchainPath.join("sysroot")
-        }
+        public let toolchainPath: AbsolutePath
+        public let sysroot: AbsolutePath
 
-        private var hostTag: String? {
+        private static func hostTag(_ host: OperatingSystem) -> String? {
             switch host {
             case .windows:
                 // Also works on Windows on ARM via Prism binary translation.
@@ -185,44 +221,119 @@ public import Foundation
             }
         }
 
-        public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
-            let ndkBasePath = sdkPath.join("ndk")
+        public struct Installations: Sendable {
+            private let preferredIndex: Int?
+            public let ndks: [NDK]
+
+            init(preferredIndex: Int? = nil, ndks: [NDK]) {
+                self.preferredIndex = preferredIndex
+                self.ndks = ndks
+            }
+
+            public var preferredNDK: NDK? {
+                preferredIndex.map { ndks[$0] } ?? ndks.only
+            }
+        }
+
+        public static func findInstallations(host: OperatingSystem, sdkPath: AbsolutePath, fs: any FSProxy) throws -> Installations {
+            if let overridePath = NDK.environmentOverrideLocation {
+                return try Installations(ndks: [NDK(host: host, path: overridePath, fs: fs)])
+            }
+
+            let ndkBasePath = sdkPath.path.join("ndk")
             guard fs.exists(ndkBasePath) else {
-                return []
+                return Installations(ndks: [])
             }
 
-            let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
-            let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }
+            var hadUnsupportedVersions: Bool = false
+            let ndks = try fs.listdir(ndkBasePath).compactMap({ subdir in
+                do {
+                    return try NDK(host: host, path: AbsolutePath(validating: ndkBasePath.join(subdir)), fs: fs)
+                } catch Error.notAnNDK(_) {
+                    return nil
+                } catch Error.unsupportedVersion(_, _) {
+                    hadUnsupportedVersions = true
+                    return nil
+                }
+            }).sorted(by: \.version)
 
-            // If we have some NDKs but all of them are unsupported, try parsing them so that parsing fails and provides a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
-            let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks
+            // If we have some NDKs but all of them are unsupported, provide a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
+            if ndks.isEmpty && hadUnsupportedVersions {
+                throw Error.noSupportedVersions(minimumVersion: Self.minimumNDKVersion)
+            }
 
-            return try discoveredNdks.map { ndkVersion in
-                let ndkPath = ndkBasePath.join(ndkVersion.description)
-                return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
+            // Respect Debian alternatives
+            let preferredIndex: Int?
+            if sdkPath == AndroidSDK.defaultDebianLocation, let ndkLinkPath = AndroidSDK.NDK.defaultDebianLocation {
+                preferredIndex = try ndks.firstIndex(where: { try $0.path.path == fs.realpath(ndkLinkPath.path) })
+            } else {
+                preferredIndex = nil
             }
+
+            return Installations(preferredIndex: preferredIndex, ndks: ndks)
         }
     }
 
     public static func findInstallations(host: OperatingSystem, fs: any FSProxy) async throws -> [AndroidSDK] {
-        let defaultLocation: Path? = switch host {
+        var paths: [AbsolutePath] = []
+        if let path = AndroidSDK.environmentOverrideLocation {
+            paths.append(path)
+        }
+        if let path = try AndroidSDK.defaultAndroidStudioLocation(host: host) {
+            paths.append(path)
+        }
+        if let path = AndroidSDK.defaultDebianLocation, host == .linux {
+            paths.append(path)
+        }
+        return try paths.compactMap { path in
+            guard fs.exists(path.path) else {
+                return nil
+            }
+            return try AndroidSDK(host: host, path: path, fs: fs)
+        }
+    }
+}
+
+fileprivate extension AndroidSDK.NDK {
+    /// The location of the Android NDK based on the `ANDROID_NDK_ROOT` environment variable (falling back to the deprecated but well known `ANDROID_NDK_HOME`).
+    /// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
+    static var environmentOverrideLocation: AbsolutePath? {
+        (getEnvironmentVariable("ANDROID_NDK_ROOT") ?? getEnvironmentVariable("ANDROID_NDK_HOME"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
+    }
+
+    /// Location of the Android NDK installed by the `google-android-ndk-*-installer` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
+    /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
+    static var defaultDebianLocation: AbsolutePath? {
+        AbsolutePath("/usr/lib/android-ndk")
+    }
+}
+
+fileprivate extension AndroidSDK {
+    /// The location of the Android SDK based on the `ANDROID_HOME` environment variable (falling back to the deprecated but well known `ANDROID_SDK_ROOT`).
+    /// - seealso: [Android environment variables](https://developer.android.com/tools/variables)
+    static var environmentOverrideLocation: AbsolutePath? {
+        (getEnvironmentVariable("ANDROID_HOME") ?? getEnvironmentVariable("ANDROID_SDK_ROOT"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
+    }
+
+    static func defaultAndroidStudioLocation(host: OperatingSystem) throws -> AbsolutePath? {
+        switch host {
         case .windows:
             // %LOCALAPPDATA%\Android\Sdk
-            try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").filePath
+            try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").absoluteFilePath
         case .macOS:
             // ~/Library/Android/sdk
-            try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").filePath
+            try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").absoluteFilePath
         case .linux:
             // ~/Android/Sdk
-            Path.homeDirectory.join("Android").join("Sdk")
+            try AbsolutePath(validating: Path.homeDirectory.join("Android").join("Sdk"))
         default:
             nil
         }
+    }
 
-        if let path = defaultLocation, fs.exists(path) {
-            return try [AndroidSDK(host: host, path: path, fs: fs)]
-        }
-
-        return []
+    /// Location of the Android SDK installed by the `google-*` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
+    /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
+    static var defaultDebianLocation: AbsolutePath? {
+        AbsolutePath("/usr/lib/android-sdk")
     }
 }
diff --git a/Sources/SWBAndroidPlatform/CMakeLists.txt b/Sources/SWBAndroidPlatform/CMakeLists.txt
index fdb0bfa7..72d8cd9b 100644
--- a/Sources/SWBAndroidPlatform/CMakeLists.txt
+++ b/Sources/SWBAndroidPlatform/CMakeLists.txt
@@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
 
 add_library(SWBAndroidPlatform
   AndroidSDK.swift
+  JavaProperties.swift
   Plugin.swift)
 SwiftBuild_Bundle(MODULE SWBAndroidPlatform FILES
   Specs/Android.xcspec)
diff --git a/Sources/SWBAndroidPlatform/JavaProperties.swift b/Sources/SWBAndroidPlatform/JavaProperties.swift
new file mode 100644
index 00000000..0a6059d7
--- /dev/null
+++ b/Sources/SWBAndroidPlatform/JavaProperties.swift
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+internal import SWBUtil
+
+/// A simple representation of a Java properties file.
+///
+/// See `java.util.Properties` for a description of the file format. This parser is a simplified version that doesn't handle line continuations, etc., because our use case is narrow.
+struct JavaProperties {
+    private let properties: [String: String]
+
+    init(data: Data) throws {
+        properties = Dictionary(uniqueKeysWithValues: String(decoding: data, as: UTF8.self).split(whereSeparator: { $0.isNewline }).map(String.init).map {
+            let (key, value) = $0.split("=")
+            return (key.trimmingCharacters(in: .whitespaces), value.trimmingCharacters(in: .whitespaces))
+        })
+    }
+
+    subscript(_ propertyName: String) -> String? {
+        properties[propertyName]
+    }
+}
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 88fa5f76..3eb0ca1d 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -11,7 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 public import SWBUtil
-import SWBCore
+public import SWBCore
 import SWBMacro
 import Foundation
 
@@ -24,7 +24,7 @@ import Foundation
     manager.register(AndroidToolchainRegistryExtension(plugin: plugin), type: ToolchainRegistryExtensionPoint.self)
 }
 
-final class AndroidPlugin: Sendable {
+@_spi(Testing) public final class AndroidPlugin: Sendable {
     private let androidSDKInstallations = AsyncCache<OperatingSystem, [AndroidSDK]>()
 
     func cachedAndroidSDKInstallations(host: OperatingSystem) async throws -> [AndroidSDK] {
@@ -33,6 +33,18 @@ final class AndroidPlugin: Sendable {
             try await AndroidSDK.findInstallations(host: host, fs: localFS)
         }
     }
+
+    @_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK, ndk: AndroidSDK.NDK)? {
+        guard let androidSdk = try? await cachedAndroidSDKInstallations(host: host).first else {
+            return nil
+        }
+
+        guard let androidNdk = androidSdk.preferredNDK else {
+            return nil
+        }
+
+        return (androidSdk, androidNdk)
+    }
 }
 
 struct AndroidPlatformSpecsExtension: SpecificationsExtension {
@@ -52,9 +64,13 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
         switch context.hostOperatingSystem {
         case .windows, .macOS, .linux:
             if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
+                let sdkPath = latest.path.path.str
+                let ndkPath = latest.preferredNDK?.path.path.str
                 return [
-                    "ANDROID_SDK_ROOT": latest.path.str,
-                    "ANDROID_NDK_ROOT": latest.ndks.last?.path.str,
+                    "ANDROID_HOME": sdkPath,
+                    "ANDROID_SDK_ROOT": sdkPath,
+                    "ANDROID_NDK_ROOT": ndkPath,
+                    "ANDROID_NDK_HOME": ndkPath,
                 ].compactMapValues { $0 }
             }
         default:
@@ -80,10 +96,10 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
     }
 }
 
-struct AndroidSDKRegistryExtension: SDKRegistryExtension {
-    let plugin: AndroidPlugin
+@_spi(Testing) public struct AndroidSDKRegistryExtension: SDKRegistryExtension {
+    @_spi(Testing) public let plugin: AndroidPlugin
 
-    func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
+    public func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
         let host = context.hostOperatingSystem
         guard let androidPlatform = context.platformRegistry.lookup(name: "android") else {
             return []
@@ -108,11 +124,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             "AR": .plString(host.imageFormat.executableName(basename: "llvm-ar")),
         ]
 
-        guard let androidSdk = try? await plugin.cachedAndroidSDKInstallations(host: host).first else {
-            return []
-        }
-
-        guard let androidNdk = androidSdk.latestNDK else {
+        guard let (_, androidNdk) = try await plugin.effectiveInstallation(host: host) else {
             return []
         }
 
@@ -150,7 +162,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             swiftSettings = [:]
         }
 
-        return [(androidNdk.sysroot, androidPlatform, [
+        return [(androidNdk.sysroot.path, androidPlatform, [
             "Type": .plString("SDK"),
             "Version": .plString("0.0.0"),
             "CanonicalName": .plString("android"),
@@ -187,7 +199,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
     let plugin: AndroidPlugin
 
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
-        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else {
+        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.preferredNDK?.toolchainPath else {
             return []
         }
 
@@ -197,13 +209,13 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
                 displayName: "Android",
                 version: Version(0, 0, 0),
                 aliases: [],
-                path: toolchainPath,
+                path: toolchainPath.path,
                 frameworkPaths: [],
                 libraryPaths: [],
                 defaultSettings: [:],
                 overrideSettings: [:],
                 defaultSettingsWhenPrimary: [:],
-                executableSearchPaths: [toolchainPath.join("bin")],
+                executableSearchPaths: [toolchainPath.path.join("bin")],
                 testingLibraryPlatformNames: [],
                 fs: context.fs)
         ]
diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift
index ddd3c5a9..d6a6ee71 100644
--- a/Sources/SWBUtil/FSProxy.swift
+++ b/Sources/SWBUtil/FSProxy.swift
@@ -879,9 +879,13 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
 
     public func realpath(_ path: Path) throws -> Path {
         // TODO: Update this to actually return the link target when we support
-        // symlinks; for now it just returns the input, which seems reasonably
-        // correct.
-        return path
+        // symlinks; for now it just returns the input (or the link target if it's a symlink),
+        // which seems reasonably correct for simple cases.
+        do {
+            return try readlink(path)
+        } catch {
+            return path
+        }
     }
 
     public func readlink(_ path: Path) throws -> Path {
diff --git a/Sources/SWBUtil/Path.swift b/Sources/SWBUtil/Path.swift
index 1edf33b7..28294f29 100644
--- a/Sources/SWBUtil/Path.swift
+++ b/Sources/SWBUtil/Path.swift
@@ -1006,6 +1006,10 @@ public struct RelativePath: Hashable, Equatable, Serializable, Sendable {
 }
 
 extension AbsolutePath {
+    public static var root: AbsolutePath {
+        AbsolutePath(.root)!
+    }
+
     public var dirname: AbsolutePath {
         AbsolutePath(path.dirname)!
     }
diff --git a/Sources/SWBUtil/URL.swift b/Sources/SWBUtil/URL.swift
index 725f7ac5..9195baa6 100644
--- a/Sources/SWBUtil/URL.swift
+++ b/Sources/SWBUtil/URL.swift
@@ -18,7 +18,7 @@ extension URL {
     /// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash.
     ///
     /// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable.
-    public var filePath: Path {
+    public var absoluteFilePath: AbsolutePath {
         get throws {
             guard isFileURL else {
                 throw FileURLError.notRepresentable(self)
@@ -27,12 +27,16 @@ extension URL {
                 guard let cString else {
                     throw FileURLError.notRepresentable(self)
                 }
-                let fp = Path(String(cString: cString))
-                precondition(fp.isAbsolute, "path '\(fp.str)' is not absolute")
-                return fp
+                return try AbsolutePath(validating: String(cString: cString))
             }
         }
     }
+
+    public var filePath: Path {
+        get throws {
+            try absoluteFilePath.path
+        }
+    }
 }
 
 fileprivate enum FileURLError: Error, CustomStringConvertible {
diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
index 37329d94..b567c294 100644
--- a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
+++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
@@ -24,22 +24,74 @@ fileprivate struct AndroidSDKTests {
         // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed
         for installation in installations {
             #expect(installation.host == host)
-            #expect(installation.latestNDK == installation.ndks.last)
         }
     }
 
-    @Test func abis_r22() async throws {
-        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in
-            let error = #expect(throws: StubError.self) {
-                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+    @Test(.skipHostOS(.windows, "This test inherently relies on Unix-style paths"))
+    func debian() async throws {
+        let fs = PseudoFS()
+        let sdkPath = try AbsolutePath(validating: "/usr/lib/android-sdk")
+        try fs.createDirectory(sdkPath.path, recursive: true)
+        try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: Version("24"), Version("25"), Version("26")) { host, fs, sdkPath, ndkVersionPaths in
+            for ndkVersionPath in ndkVersionPaths {
+                try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
+                    contents <<<
+                """
+                {
+                }
+                """
+                }
+
+                try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
+                    contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 35,
+                    "aliases": {
+                    }
+                }
+                """
+                }
             }
-            #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)")
+
+            try fs.symlink(Path("/usr/lib/android-ndk"), target: ndkVersionPaths[1].path)
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.preferredNDK)
+            #expect(installations.ndks.count == 3)
+            #expect(installation != installations.ndks.first)
+            #expect(installation == installations.ndks[1])
+            #expect(installation != installations.ndks.last)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPaths[1])
+            #expect(try installation.version == Version("25"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 35)
+
+            #expect(installation.abis.isEmpty)
+        }
+    }
+
+    @Test func unsupportedVersions() async throws {
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in
+            let error = try #require(throws: AndroidSDK.NDK.Error.self) {
+                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            }
+            #expect(error.description == "All installed NDK versions are not supported (r23 or later required)")
+        }
+
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in
+            let error = try #require(throws: AndroidSDK.NDK.Error.self) {
+                try AndroidSDK.NDK(host: host, path: ndkVersionPath, fs: fs)
+            }
+            #expect(error.description == "Android NDK version at path '\(ndkVersionPath.path.str)' is not supported (r23 or later required)")
         }
     }
 
     @Test func abis_r26_3() async throws {
-        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, sdkPath, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
                 contents <<<
                 """
                 {
@@ -83,7 +135,7 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
                 contents <<<
                 """
                 {
@@ -115,8 +167,8 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
-            let installation = try #require(installations.only)
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.ndks.only)
             #expect(installation.host == host)
             #expect(installation.path == ndkVersionPath)
             #expect(try installation.version == Version("26.3.11579264"))
@@ -180,8 +232,8 @@ fileprivate struct AndroidSDKTests {
     }
 
     @Test func abis_r27() async throws {
-        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, sdkPath, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
                 contents <<<
                 """
                 {
@@ -239,7 +291,7 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
                 contents <<<
                 """
                 {
@@ -272,8 +324,8 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
-            let installation = try #require(installations.only)
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.ndks.only)
             #expect(installation.host == host)
             #expect(installation.path == ndkVersionPath)
             #expect(try installation.version == Version("27.0.11718014"))
@@ -349,13 +401,26 @@ fileprivate struct AndroidSDKTests {
         }
     }
 
-    private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws {
-        let fs = PseudoFS()
-        let ndkPath = Path.root.join("ndk")
-        let ndkVersionPath = ndkPath.join(version.description)
-        try fs.createDirectory(ndkPath, recursive: true)
-        try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+    private func withNDKVersions(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, versions: Version..., block: (OperatingSystem, any FSProxy, AbsolutePath, [AbsolutePath]) async throws -> ()) async throws {
+        let ndkPath = sdkPath.path.join("ndk")
+        let ndkVersionPaths = try await versions.asyncMap { version in
+            let ndkVersionPath = ndkPath.join(version.description)
+            try fs.createDirectory(ndkPath, recursive: true)
+            try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+            try await fs.writeFileContents(ndkVersionPath.join("source.properties")) {
+                $0 <<< "Pkg.Desc = Android NDK\n"
+                $0 <<< "Pkg.Revision = \(version.description)-beta1\n"
+                $0 <<< "Pkg.BaseRevision = \(version.description)\n"
+            }
+            return ndkVersionPath
+        }
         let host = try ProcessInfo.processInfo.hostOperatingSystem()
-        try await block(host, fs, ndkVersionPath)
+        try await block(host, fs, sdkPath, ndkVersionPaths.map { try AbsolutePath(validating: $0) })
+    }
+
+    private func withNDKVersion(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, version: Version, _ block: (OperatingSystem, any FSProxy, AbsolutePath, AbsolutePath) async throws -> ()) async throws {
+        try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: version) { host, fs, sdkPath, ndkVersionPaths in
+            try await block(host, fs, sdkPath, ndkVersionPaths[0])
+        }
     }
 }
diff --git a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
index 74ef48e4..117588f9 100644
--- a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
+++ b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
@@ -11,6 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 import Testing
+@_spi(Testing) import SWBAndroidPlatform
 import SWBProtocol
 import SWBTestSupport
 import SWBTaskExecution
@@ -94,6 +95,12 @@ fileprivate struct AndroidBuildOperationTests: CoreBasedTests {
                     ),
                 ])
             let core = try await getCore()
+            let androidExtension = try await #require(core.pluginManager.extensions(of: SDKRegistryExtensionPoint.self).compactMap { $0 as? AndroidSDKRegistryExtension }.only)
+            let (_, androidNdk) = try #require(await androidExtension.plugin.effectiveInstallation(host: core.hostOperatingSystem))
+            if androidNdk.version < Version(27) && arch == "riscv64" {
+                return // riscv64 support was introduced in NDK r27
+            }
+
             let tester = try await BuildOperationTester(core, testProject, simulated: false)
 
             let projectDir = tester.workspace.projects[0].sourceRoot

From 9000ce167e4f1f9b8696b63cb4b75822f3f30b25 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Mon, 4 Aug 2025 10:58:45 -0700
Subject: [PATCH 32/49] Handle the OS version in the triple for FreeBSD

We're going to keep the versioned triple, so it needs to be passed through after all.
---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   |  24 ---
 Sources/SWBAndroidPlatform/Plugin.swift       |  13 +-
 Sources/SWBGenericUnixPlatform/Plugin.swift   | 138 ++++++++++++------
 Sources/SWBTestSupport/CoreTestSupport.swift  |   2 +-
 Sources/SWBUtil/CMakeLists.txt                |   1 +
 Sources/SWBUtil/LLVMTriple.swift              |  50 +++++++
 .../SWBWindowsPlatform/VSInstallation.swift   |   4 +-
 7 files changed, 153 insertions(+), 79 deletions(-)
 create mode 100644 Sources/SWBUtil/LLVMTriple.swift

diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index 3b934838..9a2c00a0 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -134,30 +134,6 @@ public import Foundation
                 case bits64 = 64
             }
 
-            @_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable {
-                public var arch: String
-                public var vendor: String
-                public var system: String
-                public var environment: String
-
-                var description: String {
-                    "\(arch)-\(vendor)-\(system)-\(environment)"
-                }
-
-                public init(from decoder: any Decoder) throws {
-                    let container = try decoder.singleValueContainer()
-                    let triple = try container.decode(String.self)
-                    if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
-                        self.arch = String(match.output.arch)
-                        self.vendor = String(match.output.vendor)
-                        self.system = String(match.output.system)
-                        self.environment = String(match.output.environment)
-                    } else {
-                        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
-                    }
-                }
-            }
-
             public let bitness: Bitness
             public let `default`: Bool
             public let deprecated: Bool
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 3eb0ca1d..117e7c6e 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -131,11 +131,14 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
         let abis = androidNdk.abis
         let deploymentTargetRange = androidNdk.deploymentTargetRange
 
-        let allPossibleTriples = abis.values.flatMap { abi in
-            (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
+        let allPossibleTriples = try abis.values.flatMap { abi in
+            try (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
                 var triple = abi.llvm_triple
                 triple.vendor = "unknown" // Android NDK uses "none", Swift SDKs use "unknown"
-                triple.environment += "\(deploymentTarget)"
+                guard let env = triple.environment else {
+                    throw StubError.error("Android triples must have an environment")
+                }
+                triple.environment = "\(env)\(deploymentTarget)"
                 return triple
             }
         }.map(\.description)
@@ -173,8 +176,8 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
             "CustomProperties": .plDict([
                 // Unlike most platforms, the Android version goes on the environment field rather than the system field
                 // FIXME: Make this configurable in a better way so we don't need to push build settings at the SDK definition level
-                "LLVM_TARGET_TRIPLE_OS_VERSION": .plString("linux"),
-                "LLVM_TARGET_TRIPLE_SUFFIX": .plString("-android$(ANDROID_DEPLOYMENT_TARGET)"),
+                "LLVM_TARGET_TRIPLE_OS_VERSION": .plString("$(SWIFT_PLATFORM_TARGET_PREFIX)"),
+                "LLVM_TARGET_TRIPLE_SUFFIX": .plString("-android$($(DEPLOYMENT_TARGET_SETTING_NAME))"),
             ].merging(swiftSettings, uniquingKeysWith: { _, new in new })),
             "SupportedTargets": .plDict([
                 "android": .plDict([
diff --git a/Sources/SWBGenericUnixPlatform/Plugin.swift b/Sources/SWBGenericUnixPlatform/Plugin.swift
index 0d6cd88c..632a4cae 100644
--- a/Sources/SWBGenericUnixPlatform/Plugin.swift
+++ b/Sources/SWBGenericUnixPlatform/Plugin.swift
@@ -15,11 +15,44 @@ import SWBCore
 import Foundation
 
 @PluginExtensionSystemActor public func initializePlugin(_ manager: PluginManager) {
+    let plugin = GenericUnixPlugin()
     manager.register(GenericUnixDeveloperDirectoryExtension(), type: DeveloperDirectoryExtensionPoint.self)
     manager.register(GenericUnixPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self)
     manager.register(GenericUnixPlatformInfoExtension(), type: PlatformInfoExtensionPoint.self)
-    manager.register(GenericUnixSDKRegistryExtension(), type: SDKRegistryExtensionPoint.self)
-    manager.register(GenericUnixToolchainRegistryExtension(), type: ToolchainRegistryExtensionPoint.self)
+    manager.register(GenericUnixSDKRegistryExtension(plugin: plugin), type: SDKRegistryExtensionPoint.self)
+    manager.register(GenericUnixToolchainRegistryExtension(plugin: plugin), type: ToolchainRegistryExtensionPoint.self)
+}
+
+final class GenericUnixPlugin: Sendable {
+    func swiftExecutablePath(fs: any FSProxy) -> Path? {
+        [
+            Environment.current["SWIFT_EXEC"].map(Path.init),
+            StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift"))
+        ].compactMap { $0 }.first(where: fs.exists)
+    }
+
+    func swiftTargetInfo(swiftExecutablePath: Path) async throws -> SwiftTargetInfo {
+        let args = ["-print-target-info"]
+        let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: swiftExecutablePath.str), arguments: args)
+        guard executionResult.exitStatus.isSuccess else {
+            throw RunProcessNonZeroExitError(args: [swiftExecutablePath.str] + args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
+        }
+        return try JSONDecoder().decode(SwiftTargetInfo.self, from: executionResult.stdout)
+    }
+}
+
+struct SwiftTargetInfo: Decodable {
+    struct TargetInfo: Decodable {
+        let triple: LLVMTriple
+        let unversionedTriple: LLVMTriple
+    }
+    let target: TargetInfo
+}
+
+extension SwiftTargetInfo.TargetInfo {
+    var tripleVersion: String? {
+        triple != unversionedTriple && triple.system.hasPrefix(unversionedTriple.system) ? String(triple.system.dropFirst(unversionedTriple.system.count)).nilIfEmpty : nil
+    }
 }
 
 struct GenericUnixDeveloperDirectoryExtension: DeveloperDirectoryExtension {
@@ -69,9 +102,11 @@ struct GenericUnixPlatformInfoExtension: PlatformInfoExtension {
 }
 
 struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
+    let plugin: GenericUnixPlugin
+
     func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
         let operatingSystem = context.hostOperatingSystem
-        guard operatingSystem.createFallbackSystemToolchain, let platform = try context.platformRegistry.lookup(name: operatingSystem.xcodePlatformName) else {
+        guard operatingSystem.createFallbackSystemToolchain, let platform = try context.platformRegistry.lookup(name: operatingSystem.xcodePlatformName), let swift = plugin.swiftExecutablePath(fs: context.fs) else {
             return []
         }
 
@@ -100,6 +135,23 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
             tripleEnvironment = ""
         }
 
+        let swiftTargetInfo = try await plugin.swiftTargetInfo(swiftExecutablePath: swift)
+
+        let deploymentTargetSettings: [String: PropertyListItem]
+        if operatingSystem == .freebsd {
+            guard let tripleVersion = swiftTargetInfo.target.tripleVersion else {
+                throw StubError.error("Unknown FreeBSD triple version")
+            }
+            deploymentTargetSettings = [
+                "DeploymentTargetSettingName": .plString("FREEBSD_DEPLOYMENT_TARGET"),
+                "DefaultDeploymentTarget": .plString(tripleVersion),
+                "MinimumDeploymentTarget": .plString(tripleVersion),
+                "MaximumDeploymentTarget": .plString(tripleVersion),
+            ]
+        } else {
+            deploymentTargetSettings = [:]
+        }
+
         return try [(.root, platform, [
             "Type": .plString("SDK"),
             "Version": .plString(Version(ProcessInfo.processInfo.operatingSystemVersion).zeroTrimmed.description),
@@ -114,62 +166,54 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
                     "LLVMTargetTripleEnvironment": .plString(tripleEnvironment),
                     "LLVMTargetTripleSys": .plString(operatingSystem.xcodePlatformName),
                     "LLVMTargetTripleVendor": .plString("unknown"),
-                ])
+                ].merging(deploymentTargetSettings, uniquingKeysWith: { _, new in new }))
             ]),
         ])]
     }
 }
 
 struct GenericUnixToolchainRegistryExtension: ToolchainRegistryExtension {
+    let plugin: GenericUnixPlugin
+
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
         let operatingSystem = context.hostOperatingSystem
-        guard operatingSystem.createFallbackSystemToolchain else {
+        let fs = context.fs
+        guard operatingSystem.createFallbackSystemToolchain, let swift = plugin.swiftExecutablePath(fs: fs) else {
             return []
         }
 
-        let fs = context.fs
-
-        for swift in [
-            Environment.current["SWIFT_EXEC"].map(Path.init),
-            StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift"))
-        ].compactMap(\.self) {
-            if fs.exists(swift) {
-                let realSwiftPath = try fs.realpath(swift).dirname.normalize()
-                let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
-                let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
-                let path: Path
-                switch (hasUsrBin, hasUsrLocalBin) {
-                case (true, false):
-                    path = realSwiftPath.dirname.dirname
-                case (false, true):
-                    path = realSwiftPath.dirname.dirname.dirname
-                case (false, false):
-                    throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
-                case (true, true):
-                    preconditionFailure()
-                }
-                let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
-                let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
-                return [
-                    Toolchain(
-                        identifier: ToolchainRegistry.defaultToolchainIdentifier,
-                        displayName: "Default",
-                        version: Version(),
-                        aliases: ["default"],
-                        path: path,
-                        frameworkPaths: [],
-                        libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
-                        defaultSettings: [:],
-                        overrideSettings: [:],
-                        defaultSettingsWhenPrimary: [:],
-                        executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
-                        testingLibraryPlatformNames: [],
-                        fs: fs)
-                ]
-            }
+        let realSwiftPath = try fs.realpath(swift).dirname.normalize()
+        let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
+        let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
+        let path: Path
+        switch (hasUsrBin, hasUsrLocalBin) {
+        case (true, false):
+            path = realSwiftPath.dirname.dirname
+        case (false, true):
+            path = realSwiftPath.dirname.dirname.dirname
+        case (false, false):
+            throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
+        case (true, true):
+            preconditionFailure()
         }
-
-        return []
+        let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
+        let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
+        return [
+            Toolchain(
+                identifier: ToolchainRegistry.defaultToolchainIdentifier,
+                displayName: "Default",
+                version: Version(),
+                aliases: ["default"],
+                path: path,
+                frameworkPaths: [],
+                libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
+                defaultSettings: [:],
+                overrideSettings: [:],
+                defaultSettingsWhenPrimary: [:],
+                executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
+                testingLibraryPlatformNames: [],
+                fs: fs)
+        ]
     }
 }
 
diff --git a/Sources/SWBTestSupport/CoreTestSupport.swift b/Sources/SWBTestSupport/CoreTestSupport.swift
index 1bf280ea..5ce4bedf 100644
--- a/Sources/SWBTestSupport/CoreTestSupport.swift
+++ b/Sources/SWBTestSupport/CoreTestSupport.swift
@@ -56,7 +56,7 @@ extension Core {
             developerPath = .xcode(xcodeDeveloperDirPath)
         } else {
             // In the context of auto-generated package schemes, try to infer the active Xcode.
-            let potentialDeveloperPath = getEnvironmentVariable("PATH")?.components(separatedBy: String(Path.pathEnvironmentSeparator)).first.map(Path.init)?.dirname.dirname
+            let potentialDeveloperPath = getEnvironmentVariable(.path)?.components(separatedBy: String(Path.pathEnvironmentSeparator)).first.map(Path.init)?.dirname.dirname
             let versionInfo = potentialDeveloperPath?.dirname.join("version.plist")
             if let versionInfo = versionInfo, (try? PropertyList.fromPath(versionInfo, fs: localFS))?.dictValue?["ProjectName"] == "IDEApplication" {
                 developerPath = potentialDeveloperPath.map { .xcode($0) }
diff --git a/Sources/SWBUtil/CMakeLists.txt b/Sources/SWBUtil/CMakeLists.txt
index 9d2d611d..681fc86e 100644
--- a/Sources/SWBUtil/CMakeLists.txt
+++ b/Sources/SWBUtil/CMakeLists.txt
@@ -55,6 +55,7 @@ add_library(SWBUtil
   LazyCache.swift
   Library.swift
   LineReader.swift
+  LLVMTriple.swift
   Lock.swift
   MachO.swift
   Math.swift
diff --git a/Sources/SWBUtil/LLVMTriple.swift b/Sources/SWBUtil/LLVMTriple.swift
new file mode 100644
index 00000000..2ec27fa5
--- /dev/null
+++ b/Sources/SWBUtil/LLVMTriple.swift
@@ -0,0 +1,50 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+public struct LLVMTriple: Decodable, Equatable, Sendable, CustomStringConvertible {
+    public var arch: String
+    public var vendor: String
+    public var system: String
+    public var environment: String?
+
+    public var description: String {
+        if let environment {
+            return "\(arch)-\(vendor)-\(system)-\(environment)"
+        }
+        return "\(arch)-\(vendor)-\(system)"
+    }
+
+    public init(_ string: String) throws {
+        guard let match = try #/(?<arch>[^-]+)-(?<vendor>[^-]+)-(?<system>[^-]+)(-(?<environment>[^-]+))?/#.wholeMatch(in: string) else {
+            throw LLVMTripleError.invalidTripleStringFormat(string)
+        }
+        self.arch = String(match.output.arch)
+        self.vendor = String(match.output.vendor)
+        self.system = String(match.output.system)
+        self.environment = match.output.environment.map { String($0) }
+    }
+
+    public init(from decoder: any Swift.Decoder) throws {
+        self = try Self(decoder.singleValueContainer().decode(String.self))
+    }
+}
+
+enum LLVMTripleError: Error, CustomStringConvertible {
+    case invalidTripleStringFormat(String)
+
+    var description: String {
+        switch self {
+        case let .invalidTripleStringFormat(tripleString):
+            "Invalid triple string format: \(tripleString)"
+        }
+    }
+}
diff --git a/Sources/SWBWindowsPlatform/VSInstallation.swift b/Sources/SWBWindowsPlatform/VSInstallation.swift
index 23ab705c..68c35abb 100644
--- a/Sources/SWBWindowsPlatform/VSInstallation.swift
+++ b/Sources/SWBWindowsPlatform/VSInstallation.swift
@@ -57,14 +57,14 @@ public struct VSInstallation: Decodable, Sendable {
         ]
         let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: vswhere.str), arguments: args)
         guard executionResult.exitStatus.isSuccess else {
-            throw RunProcessNonZeroExitError(args: args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
+            throw RunProcessNonZeroExitError(args: [vswhere.str] + args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
         }
         return try JSONDecoder().decode([VSInstallation].self, from: executionResult.stdout)
     }
 
     private static func vswherePath(fs: any FSProxy) throws -> Path? {
         var paths: [Path] = []
-        if let path = try POSIX.getenv("PATH") {
+        if let path = getEnvironmentVariable(.path) {
             paths.append(contentsOf: path.split(separator: Path.pathEnvironmentSeparator).map(Path.init).filter {
                 // PATH may contain unexpanded shell variable references
                 $0.isAbsolute

From 5c1f405dfbc174e4365bbf10ada14543e27f5689 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Mon, 4 Aug 2025 10:58:45 -0700
Subject: [PATCH 33/49] Remove unnecessary version checks

The deployment target is now macOS 14.
---
 Sources/SWBAndroidPlatform/AndroidSDK.swift         |  4 ----
 .../SpecImplementations/Tools/CodeSign.swift        |  6 ++----
 .../CodeSignTaskConstructionTests.swift             | 13 ++-----------
 3 files changed, 4 insertions(+), 19 deletions(-)

diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index 9a2c00a0..fc5d2060 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -57,10 +57,6 @@ public import Foundation
 
             let metaPath = ndkPath.path.join("meta")
 
-            guard #available(macOS 14, *) else {
-                throw StubError.error("Unsupported macOS version")
-            }
-
             if version < Self.minimumNDKVersion {
                 throw Error.unsupportedVersion(path: ndkPath, minimumVersion: Self.minimumNDKVersion)
             }
diff --git a/Sources/SWBCore/SpecImplementations/Tools/CodeSign.swift b/Sources/SWBCore/SpecImplementations/Tools/CodeSign.swift
index 373b3e93..5e56a5fb 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/CodeSign.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/CodeSign.swift
@@ -191,10 +191,8 @@ public final class CodesignToolSpec : CommandLineToolSpec, SpecIdentifierType, @
             commandLine.append(contentsOf: ["--launch-constraint-responsible", responsibleProcessLaunchConstraint.str])
         }
 
-        if cbc.producer.systemInfo?.operatingSystemVersion >= Version(14) {
-            if let libraryLoadConstraint = cbc.producer.signingSettings?.libraryConstraint {
-                commandLine.append(contentsOf: ["--library-constraint", libraryLoadConstraint.str])
-            }
+        if let libraryLoadConstraint = cbc.producer.signingSettings?.libraryConstraint {
+            commandLine.append(contentsOf: ["--library-constraint", libraryLoadConstraint.str])
         }
 
         // Add the path to the file to sign.
diff --git a/Tests/SWBTaskConstructionTests/CodeSignTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/CodeSignTaskConstructionTests.swift
index 33972fe8..65bc5eb1 100644
--- a/Tests/SWBTaskConstructionTests/CodeSignTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/CodeSignTaskConstructionTests.swift
@@ -1087,22 +1087,13 @@ fileprivate struct CodeSignTaskConstructionTests: CoreBasedTests {
         let testWorkspace = TestWorkspace("aWorkspace", projects: [testProject])
         let tester = try await TaskConstructionTester(getCore(), testWorkspace)
 
-        // Check --library-constraint is passed when the build setting is configured on macOS 14
-        await tester.checkBuild(BuildParameters(action: .install, configuration: "Release", overrides: ["LIBRARY_LOAD_CONSTRAINT": "/tmp/LibraryLoadConstraint.plist"]), runDestination: .macOS, systemInfo: SystemInfo(operatingSystemVersion: Version(14), productBuildVersion: "99A98", nativeArchitecture: "arm64")) { results in
+        // Check --library-constraint is passed
+        await tester.checkBuild(BuildParameters(action: .install, configuration: "Release", overrides: ["LIBRARY_LOAD_CONSTRAINT": "/tmp/LibraryLoadConstraint.plist"]), runDestination: .macOS) { results in
             results.checkTarget("Tool") { target in
                 results.checkTask(.matchTarget(target), .matchRuleType("CodeSign")) { task in
                     task.checkCommandLineMatches(["/usr/bin/codesign", "--force", "--sign", "-", "--entitlements", .suffix("Tool.xcent"), "--generate-entitlement-der", "--library-constraint", .suffix("LibraryLoadConstraint.plist"), "/tmp/aProject.dst/usr/local/bin/Tool"])
                 }
             }
         }
-
-        // Check --library-constraint is *not* passed when the build setting is configured on macOS 13
-        await tester.checkBuild(BuildParameters(action: .install, configuration: "Release", overrides: ["LIBRARY_LOAD_CONSTRAINT": "/tmp/LibraryLoadConstraint.plist"]), runDestination: .macOS, systemInfo: SystemInfo(operatingSystemVersion: Version(13), productBuildVersion: "99A98", nativeArchitecture: "arm64")) { results in
-            results.checkTarget("Tool") { target in
-                results.checkTask(.matchTarget(target), .matchRuleType("CodeSign")) { task in
-                    task.checkCommandLineMatches(["/usr/bin/codesign", "--force", "--sign", "-", "--entitlements", .suffix("Tool.xcent"), "--generate-entitlement-der", "/tmp/aProject.dst/usr/local/bin/Tool"])
-                }
-            }
-        }
     }
 }

From 1ed79abea0e699eadf1aa92e9ee5dca4380e6466 Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Tue, 1 Jul 2025 13:32:45 -0700
Subject: [PATCH 34/49] Add initial support for object library product type

---
 Sources/SWBCore/CMakeLists.txt                |   1 +
 Sources/SWBCore/PlannedTaskAction.swift       |   2 +
 .../SpecImplementations/LinkerSpec.swift      |  14 +-
 .../SpecImplementations/RegisterSpecs.swift   |   1 +
 .../Tools/LinkerTools.swift                   |  28 +-
 .../Tools/ObjectLibraryAssembler.swift        |  19 +
 Sources/SWBCore/Specs/CoreBuildSystem.xcspec  |   4 +
 .../SourcesTaskProducer.swift                 |  47 ++-
 .../BuildDescriptionManager.swift             |   8 +
 .../BuiltinTaskActionsExtension.swift         |   2 +
 Sources/SWBTaskExecution/CMakeLists.txt       |   2 +
 .../TaskActions/LinkerTaskAction.swift        |  96 +++++
 .../ObjectLibraryAssemblerTaskAction.swift    |  49 +++
 .../CapturingTaskGenerationDelegate.swift     |   8 +
 .../TaskPlanningTestSupport.swift             |   8 +
 Sources/SWBTestSupport/TestWorkspaces.swift   |   5 +
 Sources/SWBUniversalPlatform/CMakeLists.txt   |   1 +
 Sources/SWBUniversalPlatform/Specs/Ld.xcspec  |   1 +
 .../SWBUniversalPlatform/Specs/Libtool.xcspec |   3 +-
 .../Specs/ObjectLibraryAssembler.xcspec       |  38 ++
 .../Specs/PackageTypes.xcspec                 |  19 +
 .../Specs/ProductTypes.xcspec                 |  31 ++
 .../Specs/StandardFileTypes.xcspec            |  10 +
 .../Specs/WindowsLibtool.xcspec               |   3 +-
 .../ObjectLibraryBuildOperationTests.swift    | 363 ++++++++++++++++++
 .../CommandLineSpecPerfTests.swift            |   8 +
 Tests/SWBCoreTests/CommandLineSpecTests.swift |   4 +-
 .../ObjectLibraryTaskConstructionTests.swift  | 138 +++++++
 28 files changed, 888 insertions(+), 25 deletions(-)
 create mode 100644 Sources/SWBCore/SpecImplementations/Tools/ObjectLibraryAssembler.swift
 create mode 100644 Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift
 create mode 100644 Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift
 create mode 100644 Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec
 create mode 100644 Tests/SWBBuildSystemTests/ObjectLibraryBuildOperationTests.swift
 create mode 100644 Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift

diff --git a/Sources/SWBCore/CMakeLists.txt b/Sources/SWBCore/CMakeLists.txt
index 93ceda77..f3cabed7 100644
--- a/Sources/SWBCore/CMakeLists.txt
+++ b/Sources/SWBCore/CMakeLists.txt
@@ -139,6 +139,7 @@ add_library(SWBCore
   SpecImplementations/Tools/MergeInfoPlist.swift
   SpecImplementations/Tools/MkdirTool.swift
   SpecImplementations/Tools/ModulesVerifierTool.swift
+  SpecImplementations/Tools/ObjectLibraryAssembler.swift
   SpecImplementations/Tools/PLUtilTool.swift
   SpecImplementations/Tools/PrelinkedObjectLink.swift
   SpecImplementations/Tools/ProcessSDKImports.swift
diff --git a/Sources/SWBCore/PlannedTaskAction.swift b/Sources/SWBCore/PlannedTaskAction.swift
index c81f93ee..4844d09c 100644
--- a/Sources/SWBCore/PlannedTaskAction.swift
+++ b/Sources/SWBCore/PlannedTaskAction.swift
@@ -347,6 +347,8 @@ public protocol TaskActionCreationDelegate
     func createClangModuleVerifierInputGeneratorTaskAction() -> any PlannedTaskAction
     func createProcessSDKImportsTaskAction() -> any PlannedTaskAction
     func createValidateDependenciesTaskAction() -> any PlannedTaskAction
+    func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction
+    func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction
 }
 
 extension TaskActionCreationDelegate {
diff --git a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift
index df135901..c4a6782c 100644
--- a/Sources/SWBCore/SpecImplementations/LinkerSpec.swift
+++ b/Sources/SWBCore/SpecImplementations/LinkerSpec.swift
@@ -24,14 +24,16 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable {
             case textBased
             case framework
             case object
+            case objectLibrary
 
             public var description: String {
                 switch self {
-                case .static:       return "static library"
-                case .dynamic:      return "dynamic library"
-                case .textBased:    return "text-based stub"
-                case .framework:    return "framework"
-                case .object:       return "object file"
+                case .static:        return "static library"
+                case .dynamic:       return "dynamic library"
+                case .textBased:     return "text-based stub"
+                case .framework:     return "framework"
+                case .object:        return "object file"
+                case .objectLibrary: return "object library"
                 }
             }
         }
@@ -144,7 +146,7 @@ open class LinkerSpec : CommandLineToolSpec, @unchecked Sendable {
     /// Custom entry point for constructing linker tasks.
     public func constructLinkerTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, libraries: [LibrarySpecifier], usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) async {
         /// Delegate to the generic machinery.
-        await delegate.createTask(type: self, ruleInfo: defaultRuleInfo(cbc, delegate), commandLine: commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString), environment: environmentFromSpec(cbc, delegate), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: cbc.inputs.map({ $0.absolutePath }), outputs: [cbc.output], action: nil, execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing)
+        await delegate.createTask(type: self, ruleInfo: defaultRuleInfo(cbc, delegate), commandLine: commandLineFromTemplate(cbc, delegate, optionContext: discoveredCommandLineToolSpecInfo(cbc.producer, cbc.scope, delegate)).map(\.asString), environment: environmentFromSpec(cbc, delegate), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: cbc.inputs.map({ $0.absolutePath }), outputs: [cbc.output], action: createTaskAction(cbc, delegate), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing)
     }
 }
 
diff --git a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
index 9199c2ec..6d86dac4 100644
--- a/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
+++ b/Sources/SWBCore/SpecImplementations/RegisterSpecs.swift
@@ -59,6 +59,7 @@ public struct BuiltinSpecsExtension: SpecificationsExtension {
             LibtoolLinkerSpec.self,
             LipoToolSpec.self,
             MkdirToolSpec.self,
+            ObjectLibraryAssemblerSpec.self,
             PLUtilToolSpec.self,
             ProductPackagingToolSpec.self,
             ShellScriptToolSpec.self,
diff --git a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
index 959232e5..69a87c72 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/LinkerTools.swift
@@ -918,7 +918,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
         )
 
         // Create the task.
-        delegate.createTask(type: self, payload: payload, ruleInfo: defaultRuleInfo(cbc, delegate), commandLine: commandLine, environment: environmentFromSpec(cbc, delegate), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: inputs, outputs: outputs, action: nil, execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing)
+        delegate.createTask(type: self, payload: payload, ruleInfo: defaultRuleInfo(cbc, delegate), commandLine: commandLine, environment: environmentFromSpec(cbc, delegate), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: inputs, outputs: outputs, action: createTaskAction(cbc, delegate), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing)
     }
 
     public func constructPreviewsBlankInjectionDylibTask(
@@ -992,7 +992,7 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
             workingDirectory: cbc.producer.defaultWorkingDirectory,
             inputs: [],
             outputs: outputs,
-            action: nil,
+            action: createTaskAction(cbc, delegate),
             execDescription: resolveExecutionDescription(cbc, delegate),
             enableSandboxing: enableSandboxing
         )
@@ -1246,6 +1246,9 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
             case .object:
                 // Object files are added to linker inputs in the sources task producer.
                 return ([], [])
+            case .objectLibrary:
+                let pathFlags = specifier.absolutePathFlagsForLd()
+                return (pathFlags, [specifier.path])
             }
         }.reduce(([], [])) { (lhs, rhs) in (lhs.args + rhs.args, lhs.inputs + rhs.inputs) }
     }
@@ -1367,6 +1370,11 @@ public final class LdLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @unchec
         return LdLinkerOutputParser.self
     }
 
+    override public func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? {
+        let useResponseFile = cbc.scope.evaluate(BuiltinMacros.CLANG_USE_RESPONSE_FILE)
+        return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile)
+    }
+
     override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? {
         // The ALTERNATE_LINKER is the 'name' of the linker not the executable name, clang will find the linker binary based on name passed via -fuse-ld, but we need to discover
         // its properties by executing the actual binary. There is a common filename when the linker is not "ld" across all platforms using "ld.<ALTERNAME_LINKER>(.exe)"
@@ -1482,6 +1490,9 @@ fileprivate extension LinkerSpec.LibrarySpecifier {
         case (.object, _):
             // Object files are added to linker inputs in the sources task producer.
             return []
+        case (.objectLibrary, _):
+            // Object libraries can't be found via search paths.
+            return []
         }
     }
 
@@ -1518,6 +1529,8 @@ fileprivate extension LinkerSpec.LibrarySpecifier {
         case (.object, _):
             // Object files are added to linker inputs in the sources task producer.
             return []
+        case (.objectLibrary, _):
+            return ["@\(path.join("args.resp").str)"]
         }
     }
 }
@@ -1574,6 +1587,11 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u
         }
     }
 
+    override public func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? {
+        let useResponseFile = cbc.scope.evaluate(BuiltinMacros.LIBTOOL_USE_RESPONSE_FILE)
+        return delegate.taskActionCreationDelegate.createLinkerTaskAction(expandResponseFiles: !useResponseFile)
+    }
+
     override public func constructLinkerTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, libraries: [LibrarySpecifier], usedTools: [CommandLineToolSpec: Set<FileTypeSpec>]) async {
         var inputPaths = cbc.inputs.map({ $0.absolutePath })
         var specialArgs = [String]()
@@ -1619,6 +1637,10 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u
                 // Object files are added to linker inputs in the sources task producer and so end up in the link-file-list.
                 return []
 
+            case .objectLibrary:
+                inputPaths.append(specifier.path)
+                return ["@\(specifier.path.join("args.resp").str)"]
+
             case .framework:
                 // A static library can build against a framework, since the library in the framework could be a static library, which is valid, and we can't tell here whether it is or not.  So we leave it to libtool to do the right thing here.
                 // Also, we wouldn't want to emit an error here even if we could determine that it contained a dylib, since the target might be only using the framework to find headers.
@@ -1710,7 +1732,7 @@ public final class LibtoolLinkerSpec : GenericLinkerSpec, SpecIdentifierType, @u
             workingDirectory: cbc.producer.defaultWorkingDirectory,
             inputs: inputs,
             outputs: outputs,
-            action: nil,
+            action: createTaskAction(cbc, delegate),
             execDescription: resolveExecutionDescription(cbc, delegate),
             enableSandboxing: enableSandboxing
         )
diff --git a/Sources/SWBCore/SpecImplementations/Tools/ObjectLibraryAssembler.swift b/Sources/SWBCore/SpecImplementations/Tools/ObjectLibraryAssembler.swift
new file mode 100644
index 00000000..d228388d
--- /dev/null
+++ b/Sources/SWBCore/SpecImplementations/Tools/ObjectLibraryAssembler.swift
@@ -0,0 +1,19 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+public final class ObjectLibraryAssemblerSpec : GenericLinkerSpec, SpecIdentifierType, @unchecked Sendable {
+    public static let identifier: String = "org.swift.linkers.object-library-assembler"
+
+    public override func createTaskAction(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate) -> (any PlannedTaskAction)? {
+        return delegate.taskActionCreationDelegate.createObjectLibraryAssemblerTaskAction()
+    }
+}
diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
index 1c0f6f57..fe11ab15 100644
--- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
+++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
@@ -304,6 +304,7 @@
                     "mh_bundle",
                     staticlib,
                     "mh_object",
+                    "objectlib",
                 );
                 DefaultValue = "";
             },
@@ -1351,6 +1352,9 @@ When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [CFBundleIdenti
                     {
                         Value = "mh_object";
                     },
+                    {
+                        Value = "objectlib";
+                    },
                 );
                 DefaultValue = "";
                 ConditionFlavors = (
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
index 82af5212..154a25e7 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
@@ -356,7 +356,7 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
         // When emitting remarks, for now, a dSYM is required (<rdar://problem/45458590>)
         let dSYMForRemarks = scope.evaluate(BuiltinMacros.CLANG_GENERATE_OPTIMIZATION_REMARKS)
         let dSYM = dSYMForDebugInfo || dSYMForRemarks
-        return dSYM && scope.evaluate(BuiltinMacros.MACH_O_TYPE) != "staticlib" && scope.evaluate(BuiltinMacros.MACH_O_TYPE) != "mh_object"
+        return dSYM && !["staticlib", "mh_object", "objectlib"].contains(scope.evaluate(BuiltinMacros.MACH_O_TYPE))
     }
 
     /// Computes and returns a list of libraries to include when linking.
@@ -687,6 +687,15 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
                     xcframeworkSourcePath: xcframeworkPath,
                     privacyFile: nil
                 )
+            } else if fileType.conformsTo(identifier: "compiled.object-library") {
+                return LinkerSpec.LibrarySpecifier(
+                    kind: .objectLibrary,
+                    path: absolutePath,
+                    mode: .normal,
+                    useSearchPaths: false,
+                    swiftModulePaths: swiftModulePaths,
+                    swiftModuleAdditionalLinkerArgResponseFilePaths: swiftModuleAdditionalLinkerArgResponseFilePaths,
+                )
             } else {
                 // FIXME: Error handling.
                 return nil
@@ -1640,21 +1649,33 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
 
     /// Compute the linker to use in the given scope.
     private func getLinkerToUse(_ scope: MacroEvaluationScope) -> LinkerSpec {
-        let isStaticLib = scope.evaluate(BuiltinMacros.MACH_O_TYPE) == "staticlib"
-
-        // Return the custom linker, if specified.
-        var identifier = scope.evaluate(isStaticLib ? BuiltinMacros.LIBRARIAN : BuiltinMacros.LINKER)
-        if !identifier.isEmpty {
-            let spec = context.getSpec(identifier)
-            if let linker = spec as? LinkerSpec {
-                return linker
+        let identifier: String
+        switch scope.evaluate(BuiltinMacros.MACH_O_TYPE) {
+        case "objectlib":
+            identifier = ObjectLibraryAssemblerSpec.identifier
+        case "staticlib":
+            let override = scope.evaluate(BuiltinMacros.LIBRARIAN)
+            if !override.isEmpty {
+                let spec = context.getSpec(override)
+                if let linker = spec as? LinkerSpec {
+                    return linker
+                }
+
+                // FIXME: Emit a warning here.
             }
+            identifier = LibtoolLinkerSpec.identifier
+        default:
+            let override = scope.evaluate(BuiltinMacros.LINKER)
+            if !override.isEmpty {
+                let spec = context.getSpec(override)
+                if let linker = spec as? LinkerSpec {
+                    return linker
+                }
 
-            // FIXME: Emit a warning here.
+                // FIXME: Emit a warning here.
+            }
+            identifier = LdLinkerSpec.identifier
         }
-
-        // Return the default linker.
-        identifier = isStaticLib ? LibtoolLinkerSpec.identifier : LdLinkerSpec.identifier
         return context.getSpec(identifier) as! LinkerSpec
     }
 
diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
index 667e0e44..68013078 100644
--- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift
+++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
@@ -895,6 +895,14 @@ extension BuildSystemTaskPlanningDelegate: TaskActionCreationDelegate {
     func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
         return ValidateDependenciesTaskAction()
     }
+
+    func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction {
+        return ObjectLibraryAssemblerTaskAction()
+    }
+
+    func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction {
+        return LinkerTaskAction(expandResponseFiles: expandResponseFiles)
+    }
 }
 
 fileprivate extension BuildDescription {
diff --git a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
index fa3b298b..cd19b016 100644
--- a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
+++ b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
@@ -53,6 +53,8 @@ public struct BuiltinTaskActionsExtension: TaskActionExtension {
             38: GenericCachingTaskAction.self,
             39: ProcessSDKImportsTaskAction.self,
             40: ValidateDependenciesTaskAction.self,
+            42: ObjectLibraryAssemblerTaskAction.self,
+            43: LinkerTaskAction.self,
         ]
     }
 }
diff --git a/Sources/SWBTaskExecution/CMakeLists.txt b/Sources/SWBTaskExecution/CMakeLists.txt
index 4a766605..6ae2d684 100644
--- a/Sources/SWBTaskExecution/CMakeLists.txt
+++ b/Sources/SWBTaskExecution/CMakeLists.txt
@@ -50,8 +50,10 @@ add_library(SWBTaskExecution
   TaskActions/GenericCachingTaskAction.swift
   TaskActions/InfoPlistProcessorTaskAction.swift
   TaskActions/LinkAssetCatalogTaskAction.swift
+  TaskActions/LinkerTaskAction.swift
   TaskActions/LSRegisterURLTaskAction.swift
   TaskActions/MergeInfoPlistTaskAction.swift
+  TaskActions/ObjectLibraryAssemblerTaskAction.swift
   TaskActions/ODRAssetPackManifestTaskAction.swift
   TaskActions/PrecompileClangModuleTaskAction.swift
   TaskActions/ProcessProductEntitlementsTaskAction.swift
diff --git a/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift
new file mode 100644
index 00000000..fedbdede
--- /dev/null
+++ b/Sources/SWBTaskExecution/TaskActions/LinkerTaskAction.swift
@@ -0,0 +1,96 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+public import SWBCore
+public import SWBUtil
+
+public final class LinkerTaskAction: TaskAction {
+    
+    public override class var toolIdentifier: String {
+        return "linker"
+    }
+    
+    /// Whether response files should be expanded before invoking the linker.
+    private let expandResponseFiles: Bool
+    
+    public init(expandResponseFiles: Bool) {
+        self.expandResponseFiles = expandResponseFiles
+        super.init()
+    }
+    
+    public override func serialize<T: Serializer>(to serializer: T) {
+        serializer.beginAggregate(2)
+        serializer.serialize(expandResponseFiles)
+        super.serialize(to: serializer)
+        serializer.endAggregate()
+    }
+    
+    public required init(from deserializer: any Deserializer) throws {
+        try deserializer.beginAggregate(2)
+        self.expandResponseFiles = try deserializer.deserialize()
+        try super.init(from: deserializer)
+    }
+    
+    public override func performTaskAction(
+        _ task: any ExecutableTask,
+        dynamicExecutionDelegate: any DynamicTaskExecutionDelegate,
+        executionDelegate: any TaskExecutionDelegate,
+        clientDelegate: any TaskExecutionClientDelegate,
+        outputDelegate: any TaskOutputDelegate
+    ) async -> CommandResult {
+        var commandLine = Array(task.commandLineAsStrings)
+        
+        if expandResponseFiles {
+            do {
+                commandLine = try ResponseFiles.expandResponseFiles(
+                    commandLine,
+                    fileSystem: executionDelegate.fs,
+                    relativeTo: task.workingDirectory
+                )
+            } catch {
+                outputDelegate.emitError("Failed to expand response files: \(error.localizedDescription)")
+                return .failed
+            }
+        }
+        
+        let processDelegate = TaskProcessDelegate(outputDelegate: outputDelegate)
+        do {
+            let success = try await dynamicExecutionDelegate.spawn(
+                commandLine: commandLine,
+                environment: task.environment.bindingsDictionary,
+                workingDirectory: task.workingDirectory,
+                processDelegate: processDelegate
+            )
+            
+            if let error = processDelegate.executionError {
+                outputDelegate.emitError(error)
+                return .failed
+            }
+
+            if success {
+                if let spec = task.type as? CommandLineToolSpec, let files = task.dependencyData {
+                    do {
+                        try spec.adjust(dependencyFiles: files, for: task, fs: executionDelegate.fs)
+                    } catch {
+                        outputDelegate.emitWarning("Unable to perform dependency info modifications: \(error)")
+                    }
+                }
+            }
+
+            return success ? .succeeded : .failed
+        } catch {
+            outputDelegate.emitError("Process execution failed: \(error.localizedDescription)")
+            return .failed
+        }
+    }
+}
diff --git a/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift
new file mode 100644
index 00000000..82176362
--- /dev/null
+++ b/Sources/SWBTaskExecution/TaskActions/ObjectLibraryAssemblerTaskAction.swift
@@ -0,0 +1,49 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+public import SWBCore
+import SWBUtil
+import ArgumentParser
+
+public final class ObjectLibraryAssemblerTaskAction: TaskAction {
+    public override class var toolIdentifier: String {
+        return "assemble-object-library"
+    }
+
+    private struct Options: ParsableArguments {
+        @Argument var inputs: [Path]
+        @Option var output: Path
+    }
+
+    override public func performTaskAction(
+        _ task: any ExecutableTask,
+        dynamicExecutionDelegate: any DynamicTaskExecutionDelegate,
+        executionDelegate: any TaskExecutionDelegate,
+        clientDelegate: any TaskExecutionClientDelegate,
+        outputDelegate: any TaskOutputDelegate
+    ) async -> CommandResult {
+        do {
+            let options = try Options.parse(Array(task.commandLineAsStrings.dropFirst()))
+            try? executionDelegate.fs.remove(options.output)
+            try executionDelegate.fs.createDirectory(options.output)
+            _ = try await options.inputs.concurrentMap(maximumParallelism: 10) { input in
+                try executionDelegate.fs.copy(input, to: options.output.join(input.basename))
+            }
+            let args = options.inputs.map { $0.strWithPosixSlashes }
+            try executionDelegate.fs.write(options.output.join("args.resp"), contents: ByteString(encodingAsUTF8: ResponseFiles.responseFileContents(args: args)))
+            return .succeeded
+        } catch {
+            outputDelegate.emitError("\(error)")
+            return .failed
+        }
+    }
+}
diff --git a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
index cc8c6eee..979e947f 100644
--- a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
+++ b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
@@ -243,4 +243,12 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
     package func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
         return ValidateDependenciesTaskAction()
     }
+    
+    package func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction {
+        return ObjectLibraryAssemblerTaskAction()
+    }
+
+    package func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction {
+        return LinkerTaskAction(expandResponseFiles: expandResponseFiles)
+    }
 }
diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
index 0fdacfea..cc6505df 100644
--- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
+++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
@@ -471,6 +471,14 @@ extension TestTaskPlanningDelegate: TaskActionCreationDelegate {
     package func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
         return ValidateProductTaskAction()
     }
+
+    package func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction {
+        return ObjectLibraryAssemblerTaskAction()
+    }
+
+    package func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction {
+        return LinkerTaskAction(expandResponseFiles: expandResponseFiles)
+    }
 }
 
 package final class CancellingTaskPlanningDelegate: TestTaskPlanningDelegate, @unchecked Sendable {
diff --git a/Sources/SWBTestSupport/TestWorkspaces.swift b/Sources/SWBTestSupport/TestWorkspaces.swift
index 9dcc2015..34d55abf 100644
--- a/Sources/SWBTestSupport/TestWorkspaces.swift
+++ b/Sources/SWBTestSupport/TestWorkspaces.swift
@@ -919,6 +919,7 @@ package final class TestStandardTarget: TestInternalTarget, Sendable {
         case staticFramework
         case staticLibrary
         case objectFile
+        case objectLibrary
         case dynamicLibrary
         case bundle
         case xpcService
@@ -961,6 +962,8 @@ package final class TestStandardTarget: TestInternalTarget, Sendable {
                 return "com.apple.product-type.library.static"
             case .objectFile:
                 return "com.apple.product-type.objfile"
+            case .objectLibrary:
+                return "org.swift.product-type.library.object"
             case .dynamicLibrary:
                 return "com.apple.product-type.library.dynamic"
             case .bundle:
@@ -1030,6 +1033,8 @@ package final class TestStandardTarget: TestInternalTarget, Sendable {
                 return "lib\(name).a"
             case .objectFile:
                 return "\(name).o"
+            case .objectLibrary:
+                return "\(name).objlib"
             case .dynamicLibrary:
                 // FIXME: This should be based on the target platform, not the host. See also: <rdar://problem/29410050> Swift Build doesn't support product references with non-constant basenames
                 guard let suffix = try? ProcessInfo.processInfo.hostOperatingSystem().imageFormat.dynamicLibraryExtension else {
diff --git a/Sources/SWBUniversalPlatform/CMakeLists.txt b/Sources/SWBUniversalPlatform/CMakeLists.txt
index 90c37a6c..9a9d436e 100644
--- a/Sources/SWBUniversalPlatform/CMakeLists.txt
+++ b/Sources/SWBUniversalPlatform/CMakeLists.txt
@@ -37,6 +37,7 @@ SwiftBuild_Bundle(MODULE SWBUniversalPlatform FILES
   Specs/Ld.xcspec
   Specs/Lex.xcspec
   Specs/Libtool.xcspec
+  Specs/ObjectLibraryAssembler.xcspec
   Specs/PackageTypes.xcspec
   Specs/PBXCp.xcspec
   Specs/ProductTypes.xcspec
diff --git a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
index e1e1a60d..2f07a375 100644
--- a/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/Ld.xcspec
@@ -27,6 +27,7 @@
         ProgressDescription = Linking;
         InputFileTypes = (
             "compiled.mach-o.objfile",
+            "compiled.object-library",
             "compiled.mach-o.dylib",
             "sourcecode.text-based-dylib-definition",
             "wrapper.framework",
diff --git a/Sources/SWBUniversalPlatform/Specs/Libtool.xcspec b/Sources/SWBUniversalPlatform/Specs/Libtool.xcspec
index 28f8b8a3..d146ce6c 100644
--- a/Sources/SWBUniversalPlatform/Specs/Libtool.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/Libtool.xcspec
@@ -23,7 +23,8 @@
         ExecDescription = "Create static library $(OutputFile:file)";
         ProgressDescription = "Creating static library";
         InputFileTypes = (
-            compiled.mach-o.objfile
+            compiled.mach-o.objfile,
+            compiled.object-library
         );
         Outputs = (
             // We're a linker-like task, so we expect to be given an output path in 'OutputPath'.
diff --git a/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec b/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec
new file mode 100644
index 00000000..0a3d4758
--- /dev/null
+++ b/Sources/SWBUniversalPlatform/Specs/ObjectLibraryAssembler.xcspec
@@ -0,0 +1,38 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+(
+    {
+        Identifier = "org.swift.linkers.object-library-assembler";
+        Type = Linker;
+        Name = Ld;
+        Description = "Assembles an object library";
+        IsAbstract = Yes;
+        BinaryFormats = (
+            "mach-o",
+        );
+        CommandLine = "builtin-ObjectLibraryAssembler [options] [special-args] [inputs] --output $(OutputPath)";
+        RuleName = "AssembleObjectLibrary $(OutputPath) $(variant) $(arch)";
+        ExecDescription = "Assemble object library $(OutputFile:file)";
+        ProgressDescription = Linking;
+        InputFileTypes = (
+            "compiled.mach-o.objfile",
+        );
+        Outputs = (
+            "$(OutputPath)",
+        );
+        CommandOutputParser = "XCGenericCommandOutputParser";
+        Options = (
+
+        );
+    }
+)
diff --git a/Sources/SWBUniversalPlatform/Specs/PackageTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/PackageTypes.xcspec
index 2a4dd1fe..538da914 100644
--- a/Sources/SWBUniversalPlatform/Specs/PackageTypes.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/PackageTypes.xcspec
@@ -87,6 +87,25 @@
         };
     },
 
+    // Object Library
+    {
+        Type = PackageType;
+        Identifier = org.swift.package-type.object-library;
+        Name = "Object Library";
+        Description = "Object Library";
+        DefaultBuildSettings = {
+            EXECUTABLE_PREFIX = "";
+            EXECUTABLE_SUFFIX = "";
+            EXECUTABLE_NAME = "$(EXECUTABLE_PREFIX)$(PRODUCT_NAME)$(EXECUTABLE_VARIANT_SUFFIX)$(EXECUTABLE_SUFFIX)";
+            EXECUTABLE_PATH = "$(EXECUTABLE_NAME)";
+        };
+        ProductReference = {
+            FileType = compiled.object-library;
+            Name = "$(EXECUTABLE_NAME)";
+            IsLaunchable = NO;
+        };
+    },
+
     // CFBundle wrapper
     {
         Type = PackageType;
diff --git a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
index bd6cf9c8..e8211ed1 100644
--- a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
@@ -170,6 +170,37 @@
         );
     },
 
+    // Object library
+    {
+        Type = ProductType;
+        Identifier = org.swift.product-type.library.object;
+        Class = XCStandaloneExecutableProductType;
+        Name = "Object Library";
+        Description = "Object library";
+        IconNamePrefix = "TargetLibrary";
+        DefaultTargetName = "Object Library";
+        DefaultBuildProperties = {
+            FULL_PRODUCT_NAME = "$(EXECUTABLE_NAME)";
+            MACH_O_TYPE = "objectlib";
+            REZ_EXECUTABLE = YES;
+            EXECUTABLE_SUFFIX = ".$(EXECUTABLE_EXTENSION)";
+            EXECUTABLE_EXTENSION = "objlib";
+            PUBLIC_HEADERS_FOLDER_PATH = "/usr/local/include";
+            PRIVATE_HEADERS_FOLDER_PATH = "/usr/local/include";
+            INSTALL_PATH = "/usr/local/lib";
+            FRAMEWORK_FLAG_PREFIX = "-framework";
+            LIBRARY_FLAG_PREFIX = "-l";
+            LIBRARY_FLAG_NOSPACE = YES;
+            SKIP_INSTALL = YES;
+            STRIP_STYLE = "debugging";
+            GCC_INLINES_ARE_PRIVATE_EXTERN = YES;
+            CODE_SIGNING_ALLOWED = NO;
+        };
+        PackageTypes = (
+            org.swift.package-type.object-library
+        );
+    },
+
     //
     // Wrapper product types
     //
diff --git a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec
index 490c60f6..9e400f27 100644
--- a/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/StandardFileTypes.xcspec
@@ -853,6 +853,16 @@
         AppliesToBuildRules = yes;
         UTI = "com.apple.mach-o-object";
     },
+    {
+        Type = FileType;
+        Identifier = compiled.object-library;
+        Class = PBXMachOFileType;
+        BasedOn = compiled.mach-o;
+        Name = "Object Library";
+        Extensions = (o);
+        AppliesToBuildRules = yes;
+        UTI = "org.swift.object-library";
+    },
     {
         Type = FileType;
         Identifier = compiled.mach-o.executable;
diff --git a/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec b/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec
index b5257af4..4863b70f 100644
--- a/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec
+++ b/Sources/SWBWindowsPlatform/Specs/WindowsLibtool.xcspec
@@ -28,7 +28,8 @@
         ProgressDescription = "Creating static library";
         InputFileTypes = (
             // TODO: elf files instead
-            compiled.mach-o.objfile
+            compiled.mach-o.objfile,
+            compiled.object-library
         );
         Outputs = (
             // We're a linker-like task, so we expect to be given an output path in 'OutputPath'.
diff --git a/Tests/SWBBuildSystemTests/ObjectLibraryBuildOperationTests.swift b/Tests/SWBBuildSystemTests/ObjectLibraryBuildOperationTests.swift
new file mode 100644
index 00000000..37228924
--- /dev/null
+++ b/Tests/SWBBuildSystemTests/ObjectLibraryBuildOperationTests.swift
@@ -0,0 +1,363 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Testing
+
+import SWBCore
+import SWBTestSupport
+import SwiftBuildTestSupport
+@_spi(Testing) import SWBUtil
+import SWBProtocol
+import SWBTaskExecution
+
+@Suite
+fileprivate struct ObjectLibraryBuildOperationTests: CoreBasedTests {
+    @Test(.requireSDKs(.host))
+    func objectLibraryBasics() async throws {
+        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
+            let testWorkspace = TestWorkspace(
+                "Test",
+                sourceRoot: tmpDirPath.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources",
+                            children: [
+                                TestFile("a.c"),
+                                TestFile("b.c"),
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "CODE_SIGNING_ALLOWED": "NO",
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                ]),
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "Library",
+                                type: .objectLibrary,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "a.c",
+                                        "b.c",
+                                    ]),
+                                ]
+                            ),
+                        ])
+                ])
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/a.c")) {
+                $0 <<< "void foo(void) {}\n"
+            }
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/b.c")) {
+                $0 <<< "void bar(void) {}\n"
+            }
+
+            try await tester.checkBuild(runDestination: .host) { results in
+                results.checkNoDiagnostics()
+                let libPath = tmpDirPath.join("Test/aProject/build/Debug\(RunDestinationInfo.host.builtProductsDirSuffix)/Library.objlib")
+                #expect(tester.fs.exists(libPath))
+                try #expect(tester.fs.listdir(libPath).sorted() == ["a.o", "args.resp", "b.o"])
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.host))
+    func consumingObjectLibrary_ld() async throws {
+        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
+            let testWorkspace = TestWorkspace(
+                "Test",
+                sourceRoot: tmpDirPath.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources",
+                            children: [
+                                TestFile("a.swift"),
+                                TestFile("b.swift"),
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "CODE_SIGNING_ALLOWED": "NO",
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "SWIFT_VERSION": try await swiftVersion,
+                                ]),
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "Tool",
+                                type: .commandLineTool,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "b.swift",
+                                    ]),
+                                    TestFrameworksBuildPhase([
+                                        "Library.objlib"
+                                    ])
+                                ],
+                                dependencies: [
+                                    "Library",
+                                ]
+                            ),
+                            TestStandardTarget(
+                                "Library",
+                                type: .objectLibrary,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "a.swift",
+                                    ]),
+                                ]
+                            ),
+                        ])
+                ])
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/a.swift")) {
+                $0 <<< """
+                    public struct Foo {
+                        public var x: Int
+
+                        public init(x: Int) {
+                            self.x = x
+                        }
+                    }
+                """
+            }
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/b.swift")) {
+                $0 <<< """
+                    import Library
+
+                    @main
+                    struct Entry {
+                        static func main() {
+                            let f = Foo(x: 42)
+                            print(f)
+                        }
+                    }
+
+                """
+            }
+
+            try await tester.checkBuild(runDestination: .host) { results in
+                results.checkNoDiagnostics()
+                results.checkTaskExists(.matchRuleType("Ld"))
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.host))
+    func consumingObjectLibrary_libtool() async throws {
+        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
+            let testWorkspace = TestWorkspace(
+                "Test",
+                sourceRoot: tmpDirPath.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources",
+                            children: [
+                                TestFile("a.swift"),
+                                TestFile("b.swift"),
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "CODE_SIGNING_ALLOWED": "NO",
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "SWIFT_VERSION": try await swiftVersion,
+                                    "LIBTOOL_USE_RESPONSE_FILE": "NO",
+                                ]),
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "StaticLibrary",
+                                type: .staticLibrary,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "b.swift",
+                                    ]),
+                                    TestFrameworksBuildPhase([
+                                        "Library.objlib"
+                                    ])
+                                ],
+                                dependencies: [
+                                    "Library",
+                                ]
+                            ),
+                            TestStandardTarget(
+                                "Library",
+                                type: .objectLibrary,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "a.swift",
+                                    ]),
+                                ]
+                            ),
+                        ])
+                ])
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/a.swift")) {
+                $0 <<< """
+                    public struct Foo {
+                        public var x: Int
+
+                        public init(x: Int) {
+                            self.x = x
+                        }
+                    }
+                """
+            }
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/b.swift")) {
+                $0 <<< """
+                    import Library
+
+                    @main
+                    struct Entry {
+                        static func main() {
+                            let f = Foo(x: 42)
+                            print(f)
+                        }
+                    }
+
+                """
+            }
+
+            try await tester.checkBuild(runDestination: .host) { results in
+                results.checkNoDiagnostics()
+                results.checkTaskExists(.matchRuleType("Libtool"))
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.macOS))
+    func consumingObjectLibraryIncrementalBuild() async throws {
+        try await withTemporaryDirectory { tmpDirPath async throws -> Void in
+            let testWorkspace = TestWorkspace(
+                "Test",
+                sourceRoot: tmpDirPath.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources",
+                            children: [
+                                TestFile("a.swift"),
+                                TestFile("b.swift"),
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "CODE_SIGNING_ALLOWED": "NO",
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "SWIFT_VERSION": try await swiftVersion,
+                                ]),
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "Tool",
+                                type: .commandLineTool,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "b.swift",
+                                    ]),
+                                    TestFrameworksBuildPhase([
+                                        "Library.objlib"
+                                    ])
+                                ],
+                                dependencies: [
+                                    "Library",
+                                ]
+                            ),
+                            TestStandardTarget(
+                                "Library",
+                                type: .objectLibrary,
+                                buildPhases: [
+                                    TestSourcesBuildPhase([
+                                        "a.swift",
+                                    ]),
+                                ]
+                            ),
+                        ])
+                ])
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/a.swift")) {
+                $0 <<< """
+                    public struct Foo {
+                        public var x: Int
+
+                        public init(x: Int) {
+                            self.x = x
+                        }
+                    }
+                """
+            }
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/b.swift")) {
+                $0 <<< """
+                    import Library
+
+                    @main
+                    struct Entry {
+                        static func main() {
+                            let f = Foo(x: 42)
+                            print(f)
+                        }
+                    }
+
+                """
+            }
+
+            try await tester.checkBuild(runDestination: .host, persistent: true) { results in
+                results.checkNoDiagnostics()
+            }
+
+            try await tester.checkNullBuild(runDestination: .host, persistent: true)
+
+            try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/a.swift")) {
+                $0 <<< """
+                    public struct Foo {
+                        public var x: Int
+
+                        public init(x: Int) {
+                            print("hello, world!")
+                            self.x = x
+                        }
+                    }
+                """
+            }
+
+            try await tester.checkBuild(runDestination: .host, persistent: true) { results in
+                results.checkNoDiagnostics()
+                // We should both reassemble the object library and relink the executable after updating an object file.
+                results.checkTaskExists(.matchRuleType("AssembleObjectLibrary"))
+                results.checkTaskExists(.matchRuleType("Ld"))
+            }
+
+            try await tester.checkNullBuild(runDestination: .host, persistent: true)
+        }
+    }
+}
diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
index c3094eb8..75c1d8ea 100644
--- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
+++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
@@ -237,6 +237,14 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
     func createValidateDependenciesTaskAction() -> any PlannedTaskAction {
         return ValidateProductTaskAction()
     }
+
+    func createObjectLibraryAssemblerTaskAction() -> any PlannedTaskAction {
+        return ObjectLibraryAssemblerTaskAction()
+    }
+
+    func createLinkerTaskAction(expandResponseFiles: Bool) -> any PlannedTaskAction {
+        return LinkerTaskAction(expandResponseFiles: expandResponseFiles)
+    }
 }
 
 extension CapturingTaskGenerationDelegate: CoreClientDelegate {
diff --git a/Tests/SWBCoreTests/CommandLineSpecTests.swift b/Tests/SWBCoreTests/CommandLineSpecTests.swift
index c00dfc25..754f3cfc 100644
--- a/Tests/SWBCoreTests/CommandLineSpecTests.swift
+++ b/Tests/SWBCoreTests/CommandLineSpecTests.swift
@@ -1234,7 +1234,7 @@ import SWBMacro
         let mockFileType = try core.specRegistry.getSpec("file") as FileTypeSpec
         let cbc = CommandBuildContext(producer: producer, scope: mockScope, inputs: [FileToBuild(absolutePath: Path.root.join("tmp/obj/normal/x86_64/file1.o"), fileType: mockFileType)], output: Path.root.join("tmp/obj/normal/x86_64/output"))
 
-        // Test all permutations of library kind, linkage mode and search path usage, except for object files.
+        // Test all permutations of library kind, linkage mode and search path usage, except for object files and object libraries.
         func generateLibrarySpecifiers(kind: LinkerSpec.LibrarySpecifier.Kind) -> [LinkerSpec.LibrarySpecifier] {
             var result = [LinkerSpec.LibrarySpecifier]()
             for useSearchPaths in [true, false] {
@@ -1253,6 +1253,8 @@ import SWBMacro
                         filePath = Path.root.join("tmp/Foo\(suffix).framework")
                     case .object:
                         continue
+                    case .objectLibrary:
+                        continue
                     }
                     result.append(LinkerSpec.LibrarySpecifier(kind: kind, path: filePath, mode: mode, useSearchPaths: useSearchPaths, swiftModulePaths: [:], swiftModuleAdditionalLinkerArgResponseFilePaths: [:]))
                 }
diff --git a/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift
new file mode 100644
index 00000000..e6d0d958
--- /dev/null
+++ b/Tests/SWBTaskConstructionTests/ObjectLibraryTaskConstructionTests.swift
@@ -0,0 +1,138 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Testing
+
+import SWBCore
+import SWBTaskConstruction
+import SWBTestSupport
+import SWBUtil
+
+@Suite
+fileprivate struct ObjectLibraryTaskConstructionTests: CoreBasedTests {
+    @Test(.requireSDKs(.host))
+    func objectLibraryBasics() async throws {
+        let testProject = TestProject(
+            "aProject",
+            groupTree: TestGroup(
+                "SomeFiles",
+                children: [
+                    TestFile("a.c"),
+                    TestFile("b.c"),
+                ]),
+            buildConfigurations: [
+                TestBuildConfiguration("Debug", buildSettings: [
+                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                    "SWIFT_EXEC": try await swiftCompilerPath.str,
+                    "SWIFT_VERSION": try await swiftVersion
+                ]),
+            ],
+            targets: [
+                TestStandardTarget(
+                    "Library",
+                    type: .objectLibrary,
+                    buildConfigurations: [
+                        TestBuildConfiguration("Debug", buildSettings: [:]),
+                    ],
+                    buildPhases: [
+                        TestSourcesBuildPhase(["a.c", "b.c"]),
+                    ]
+                ),
+            ])
+        let core = try await getCore()
+        let tester = try TaskConstructionTester(core, testProject)
+
+        await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: [:]), runDestination: .host) { results in
+            results.checkNoDiagnostics()
+            results.checkTask(.matchRuleType("AssembleObjectLibrary")) { task in
+                task.checkCommandLineMatches([
+                    "builtin-ObjectLibraryAssembler",
+                    .suffix("a.o"),
+                    .suffix("b.o"),
+                    "--output",
+                    .suffix("Library.objlib")
+                ])
+                task.checkInputs([
+                    .pathPattern(.suffix("a.o")),
+                    .pathPattern(.suffix("b.o")),
+                    .namePattern(.any),
+                    .namePattern(.any),
+                ])
+                task.checkOutputs([
+                    .pathPattern(.suffix("Library.objlib"))
+                ])
+            }
+        }
+    }
+
+    @Test(.requireSDKs(.host))
+    func objectLibraryConsumer() async throws {
+        let testWorkspace = TestWorkspace(
+            "Test",
+            projects: [
+                TestProject(
+                    "aProject",
+                    groupTree: TestGroup(
+                        "Sources",
+                        children: [
+                            TestFile("a.swift"),
+                            TestFile("b.swift"),
+                        ]),
+                    buildConfigurations: [
+                        TestBuildConfiguration(
+                            "Debug",
+                            buildSettings: [
+                                "CODE_SIGNING_ALLOWED": "NO",
+                                "PRODUCT_NAME": "$(TARGET_NAME)",
+                                "SWIFT_VERSION": try await swiftVersion,
+                                "SWIFT_EXEC": try await swiftCompilerPath.str
+                            ]),
+                    ],
+                    targets: [
+                        TestStandardTarget(
+                            "Tool",
+                            type: .commandLineTool,
+                            buildPhases: [
+                                TestSourcesBuildPhase([
+                                    "b.swift",
+                                ]),
+                                TestFrameworksBuildPhase([
+                                    "Library.objlib"
+                                ])
+                            ],
+                            dependencies: [
+                                "Library",
+                            ]
+                        ),
+                        TestStandardTarget(
+                            "Library",
+                            type: .objectLibrary,
+                            buildPhases: [
+                                TestSourcesBuildPhase([
+                                    "a.swift",
+                                ]),
+                            ]
+                        ),
+                    ])
+            ])
+
+        let core = try await getCore()
+        let tester = try TaskConstructionTester(core, testWorkspace)
+
+        await tester.checkBuild(BuildParameters(configuration: "Debug", overrides: [:]), runDestination: .host) { results in
+            results.checkNoDiagnostics()
+            results.checkTask(.matchRuleType("Ld")) { task in
+                task.checkCommandLineMatches([.and(.suffix("args.resp"), .prefix("@"))])
+            }
+        }
+    }
+}

From eef62476dacdd7074a63971b741b9a0d5138f961 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 14 Feb 2025 15:30:25 +0900
Subject: [PATCH 35/49] Add FreeBSD support

This allows building Swift Build for FreeBSD hosts, as well as building for a FreeBSD target from a FreeBSD host.

Also adds some speculative support for targeting OpenBSD on OpenBSD hosts, since SwiftPM has minimal support.
---
 Package.swift                                 |  4 +--
 Sources/SWBCore/Settings/Settings.swift       |  4 +++
 Sources/SWBGenericUnixPlatform/Plugin.swift   | 12 ++++++---
 .../Specs/FreeBSDLibtool.xcspec               | 27 +++++++++++++++++++
 .../RunDestinationTestSupport.swift           | 20 ++++++++++++++
 .../SWBTestSupport/SkippedTestSupport.swift   | 12 +++++++--
 Sources/SWBUtil/Architecture.swift            | 13 ++++++++-
 Sources/SWBUtil/FSProxy.swift                 |  8 ++++++
 Sources/SWBUtil/Lock.swift                    |  2 +-
 Sources/SWBUtil/Process.swift                 |  2 ++
 Sources/SWBUtil/ProcessInfo.swift             |  8 +++++-
 .../BuildTaskBehaviorTests.swift              |  2 +-
 ...mmandLineToolSpecDiscoveredInfoTests.swift |  2 +-
 .../SWBCoreTests/FileTextEncodingTests.swift  |  3 +--
 Tests/SWBCoreTests/SettingsTests.swift        |  2 +-
 Tests/SWBTaskExecutionTests/PBXCpTests.swift  |  2 +-
 Tests/SWBUtilTests/ElapsedTimerTests.swift    |  3 ++-
 Tests/SWBUtilTests/FSProxyTests.swift         |  4 +--
 Tests/SWBUtilTests/FileHandleTests.swift      |  2 +-
 Tests/SWBUtilTests/HeavyCacheTests.swift      |  5 ++--
 Tests/SWBUtilTests/MiscTests.swift            |  2 +-
 Tests/SWBUtilTests/RateLimiterTests.swift     |  4 ++-
 22 files changed, 118 insertions(+), 25 deletions(-)
 create mode 100644 Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec

diff --git a/Package.swift b/Package.swift
index 8fdc5af7..5ca44540 100644
--- a/Package.swift
+++ b/Package.swift
@@ -110,7 +110,7 @@ let package = Package(
                 "SWBBuildSystem",
                 "SWBServiceCore",
                 "SWBTaskExecution",
-                .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .android, .windows])),
+                .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])),
             ],
             exclude: ["CMakeLists.txt"],
             swiftSettings: swiftSettings(languageMode: .v5)),
@@ -201,7 +201,7 @@ let package = Package(
                 "SWBCSupport",
                 "SWBLibc",
                 .product(name: "ArgumentParser", package: "swift-argument-parser"),
-                .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .android, .windows])),
+                .product(name: "SystemPackage", package: "swift-system", condition: .when(platforms: [.linux, .openbsd, .android, .windows, .custom("freebsd")])),
             ],
             exclude: ["CMakeLists.txt"],
             swiftSettings: swiftSettings(languageMode: .v5)),
diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift
index 42fcbbe9..7e4e6f07 100644
--- a/Sources/SWBCore/Settings/Settings.swift
+++ b/Sources/SWBCore/Settings/Settings.swift
@@ -5331,6 +5331,10 @@ extension OperatingSystem {
                 return "windows"
             case .linux:
                 return "linux"
+            case .freebsd:
+                return "freebsd"
+            case .openbsd:
+                return "openbsd"
             case .android:
                 return "android"
             case .unknown:
diff --git a/Sources/SWBGenericUnixPlatform/Plugin.swift b/Sources/SWBGenericUnixPlatform/Plugin.swift
index 128fde75..a3839068 100644
--- a/Sources/SWBGenericUnixPlatform/Plugin.swift
+++ b/Sources/SWBGenericUnixPlatform/Plugin.swift
@@ -39,7 +39,11 @@ struct GenericUnixPlatformSpecsExtension: SpecificationsExtension {
     }
 
     func specificationDomains() -> [String: [String]] {
-        ["linux": ["generic-unix"]]
+        [
+            "linux": ["generic-unix"],
+            "freebsd": ["generic-unix"],
+            "openbsd": ["generic-unix"],
+        ]
     }
 }
 
@@ -73,9 +77,9 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
 
         let defaultProperties: [String: PropertyListItem]
         switch operatingSystem {
-        case .linux:
+        case .linux, .freebsd:
             defaultProperties = [
-                // Workaround to avoid `-dependency_info` on Linux.
+                // Workaround to avoid `-dependency_info`.
                 "LD_DEPENDENCY_INFO_FILE": .plString(""),
 
                 "GENERATE_TEXT_BASED_STUBS": "NO",
@@ -167,6 +171,6 @@ struct GenericUnixToolchainRegistryExtension: ToolchainRegistryExtension {
 extension OperatingSystem {
     /// Whether the Core is allowed to create a fallback toolchain, SDK, and platform for this operating system in cases where no others have been provided.
     var createFallbackSystemToolchain: Bool {
-        return self == .linux
+        return self == .linux || self == .freebsd || self == .openbsd
     }
 }
diff --git a/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec b/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec
new file mode 100644
index 00000000..8d247c89
--- /dev/null
+++ b/Sources/SWBGenericUnixPlatform/Specs/FreeBSDLibtool.xcspec
@@ -0,0 +1,27 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+(
+    {
+        Domain = freebsd;
+        Identifier = com.apple.pbx.linkers.libtool;
+        BasedOn = generic-unix:com.apple.pbx.linkers.libtool;
+        Type = Linker;
+        Options = (
+            {
+                Name = "LIBTOOL_USE_RESPONSE_FILE";
+                Type = Boolean;
+                DefaultValue = NO;
+            },
+        );
+    },
+)
diff --git a/Sources/SWBTestSupport/RunDestinationTestSupport.swift b/Sources/SWBTestSupport/RunDestinationTestSupport.swift
index ecdff852..69b83184 100644
--- a/Sources/SWBTestSupport/RunDestinationTestSupport.swift
+++ b/Sources/SWBTestSupport/RunDestinationTestSupport.swift
@@ -98,6 +98,10 @@ extension _RunDestinationInfo {
             windows
         case .linux:
             linux
+        case .freebsd:
+            freebsd
+        case .openbsd:
+            openbsd
         case .android:
             android
         case .unknown:
@@ -259,6 +263,22 @@ extension _RunDestinationInfo {
         return .init(platform: "linux", sdk: "linux", sdkVariant: "linux", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false)
     }
 
+    /// A run destination targeting FreeBSD generic device, using the public SDK.
+    package static var freebsd: Self {
+        guard let arch = Architecture.hostStringValue  else {
+            preconditionFailure("Unknown architecture \(Architecture.host.stringValue ?? "<nil>")")
+        }
+        return .init(platform: "freebsd", sdk: "freebsd", sdkVariant: "freebsd", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false)
+    }
+
+    /// A run destination targeting OpenBSD generic device, using the public SDK.
+    package static var openbsd: Self {
+        guard let arch = Architecture.hostStringValue  else {
+            preconditionFailure("Unknown architecture \(Architecture.host.stringValue ?? "<nil>")")
+        }
+        return .init(platform: "openbsd", sdk: "openbsd", sdkVariant: "openbsd", targetArchitecture: arch, supportedArchitectures: ["x86_64", "aarch64"], disableOnlyActiveArch: false)
+    }
+
     /// A run destination targeting Android generic device, using the public SDK.
     package static var android: Self {
         return .init(platform: "android", sdk: "android", sdkVariant: "android", targetArchitecture: "undefined_arch", supportedArchitectures: ["armv7", "aarch64", "riscv64", "i686", "x86_64"], disableOnlyActiveArch: true)
diff --git a/Sources/SWBTestSupport/SkippedTestSupport.swift b/Sources/SWBTestSupport/SkippedTestSupport.swift
index 4d82e95f..1db40901 100644
--- a/Sources/SWBTestSupport/SkippedTestSupport.swift
+++ b/Sources/SWBTestSupport/SkippedTestSupport.swift
@@ -49,6 +49,10 @@ extension KnownSDK {
             return windows
         case .success(.linux):
             return linux
+        case .success(.freebsd):
+            return freebsd
+        case .success(.openbsd):
+            return openbsd
         case .success(.android):
             return android
         case .success(.unknown), .failure:
@@ -69,6 +73,8 @@ extension KnownSDK {
 extension KnownSDK {
     package static let windows: Self = "windows"
     package static let linux: Self = "linux"
+    package static let freebsd: Self = "freebsd"
+    package static let openbsd: Self = "openbsd"
     package static let android: Self = "android"
     package static let qnx: Self = "qnx"
     package static let wasi: Self = "wasi"
@@ -196,7 +202,7 @@ extension Trait where Self == Testing.ConditionTrait {
         }
     }
 
-    package static func requireSystemPackages(apt: String..., yum: String..., sourceLocation: SourceLocation = #_sourceLocation) -> Self {
+    package static func requireSystemPackages(apt: String..., yum: String..., freebsd: String..., sourceLocation: SourceLocation = #_sourceLocation) -> Self {
         enabled("required system packages are not installed") {
             func checkInstalled(hostOS: OperatingSystem, packageManagerPath: Path, args: [String], packages: [String], regex: Regex<(Substring, name: Substring)>) async throws -> Bool {
                 if try ProcessInfo.processInfo.hostOperatingSystem() == hostOS && localFS.exists(packageManagerPath) {
@@ -222,7 +228,9 @@ extension Trait where Self == Testing.ConditionTrait {
             // spelled `--installed` in newer versions of yum, but Amazon Linux 2 is on older versions
             let yum = try await checkInstalled(hostOS: .linux, packageManagerPath: Path("/usr/bin/yum"), args: ["list", "installed", "yum"], packages: yum, regex: #/(?<name>.+)\./#)
 
-            return apt && yum
+            let freebsd = try await checkInstalled(hostOS: .freebsd, packageManagerPath: Path("/usr/sbin/pkg"), args: ["info"], packages: freebsd, regex: #/^Name(?:[ ]+): (?<name>.+)$/#)
+
+            return apt && yum && freebsd
         }
     }
 
diff --git a/Sources/SWBUtil/Architecture.swift b/Sources/SWBUtil/Architecture.swift
index 340491bd..d7518ec8 100644
--- a/Sources/SWBUtil/Architecture.swift
+++ b/Sources/SWBUtil/Architecture.swift
@@ -98,7 +98,18 @@ public struct Architecture: Sendable {
             if uname(&buf) == 0 {
                 return withUnsafeBytes(of: &buf.machine) { buf in
                     let data = Data(buf)
-                    return String(decoding: data[0...(data.lastIndex(where: { $0 != 0 }) ?? 0)], as: UTF8.self)
+                    let value = String(decoding: data[0...(data.lastIndex(where: { $0 != 0 }) ?? 0)], as: UTF8.self)
+                    #if os(FreeBSD)
+                    switch value {
+                    case "amd64":
+                        return "x86_64"
+                    case "arm64":
+                        return "aarch64"
+                    default:
+                        break
+                    }
+                    #endif
+                    return value
                 }
             }
             return nil
diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift
index adf69e3a..9f0eb4c2 100644
--- a/Sources/SWBUtil/FSProxy.swift
+++ b/Sources/SWBUtil/FSProxy.swift
@@ -718,6 +718,9 @@ class LocalFS: FSProxy, @unchecked Sendable {
         #if os(Windows)
         // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166
         return []
+        #elseif os(FreeBSD)
+        // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836
+        return []
         #elseif os(OpenBSD)
         // OpenBSD no longer supports extended attributes
         return []
@@ -758,6 +761,8 @@ class LocalFS: FSProxy, @unchecked Sendable {
     func setExtendedAttribute(_ path: Path, key: String, value: ByteString) throws {
         #if os(Windows)
         // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166
+        #elseif os(FreeBSD)
+        // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836
         #elseif os(OpenBSD)
         // OpenBSD no longer supports extended attributes
         #else
@@ -778,6 +783,9 @@ class LocalFS: FSProxy, @unchecked Sendable {
         #if os(Windows)
         // Implement ADS on Windows? See also https://github.com/swiftlang/swift-foundation/issues/1166
         return nil
+        #elseif os(FreeBSD)
+        // FreeBSD blocked on https://github.com/swiftlang/swift/pull/77836
+        return nil
         #elseif os(OpenBSD)
         // OpenBSD no longer supports extended attributes
         return nil
diff --git a/Sources/SWBUtil/Lock.swift b/Sources/SWBUtil/Lock.swift
index abc1664a..b45625c6 100644
--- a/Sources/SWBUtil/Lock.swift
+++ b/Sources/SWBUtil/Lock.swift
@@ -28,7 +28,7 @@ public final class Lock: @unchecked Sendable {
     #if os(Windows)
     @usableFromInline
     let mutex: UnsafeMutablePointer<SRWLOCK> = UnsafeMutablePointer.allocate(capacity: 1)
-    #elseif os(OpenBSD)
+    #elseif os(FreeBSD) || os(OpenBSD)
     @usableFromInline
     let mutex: UnsafeMutablePointer<pthread_mutex_t?> = UnsafeMutablePointer.allocate(capacity: 1)
     #else
diff --git a/Sources/SWBUtil/Process.swift b/Sources/SWBUtil/Process.swift
index d07f8e9f..d832eb5a 100644
--- a/Sources/SWBUtil/Process.swift
+++ b/Sources/SWBUtil/Process.swift
@@ -70,6 +70,8 @@ extension Process {
             case .linux:
                 // Amazon Linux 2 has glibc 2.26, and glibc 2.29 is needed for posix_spawn_file_actions_addchdir_np support
                 FileManager.default.contents(atPath: "/etc/system-release").map { String(decoding: $0, as: UTF8.self) == "Amazon Linux release 2 (Karoo)\n" } ?? false
+            case .openbsd:
+                true
             default:
                 false
             }
diff --git a/Sources/SWBUtil/ProcessInfo.swift b/Sources/SWBUtil/ProcessInfo.swift
index 85b4ef2c..e96f8631 100644
--- a/Sources/SWBUtil/ProcessInfo.swift
+++ b/Sources/SWBUtil/ProcessInfo.swift
@@ -99,6 +99,10 @@ extension ProcessInfo {
         return .windows
         #elseif os(Linux)
         return .linux
+        #elseif os(FreeBSD)
+        return .freebsd
+        #elseif os(OpenBSD)
+        return .openbsd
         #else
         if try FileManager.default.isReadableFile(atPath: systemVersionPlistURL.filePath.str) {
             switch try systemVersion().productName {
@@ -129,6 +133,8 @@ public enum OperatingSystem: Hashable, Sendable {
     case visionOS(simulator: Bool)
     case windows
     case linux
+    case freebsd
+    case openbsd
     case android
     case unknown
 
@@ -157,7 +163,7 @@ public enum OperatingSystem: Hashable, Sendable {
             return .macho
         case .windows:
             return .pe
-        case .linux, .android, .unknown:
+        case .linux, .freebsd, .openbsd, .android, .unknown:
             return .elf
         }
     }
diff --git a/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift b/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift
index edc2d359..a101fdee 100644
--- a/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildTaskBehaviorTests.swift
@@ -307,7 +307,7 @@ fileprivate struct BuildTaskBehaviorTests: CoreBasedTests {
     }
 
     /// Check that we honor specs which are unsafe to interrupt.
-    @Test(.requireSDKs(.host), .skipHostOS(.windows, "no bash shell"))
+    @Test(.requireSDKs(.host), .skipHostOS(.windows, "no bash shell"), .skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
     func unsafeToInterrupt() async throws {
         let fs = localFS
         let output = MakePlannedVirtualNode("<WAIT>")
diff --git a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift
index 169c89c7..1b541d75 100644
--- a/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift
+++ b/Tests/SWBCoreTests/CommandLineToolSpecDiscoveredInfoTests.swift
@@ -242,7 +242,7 @@ import SWBMacro
         }
     }
 
-    @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool"))
+    @Test(.skipHostOS(.windows), .requireSystemPackages(apt: "libtool", yum: "libtool", freebsd: "libtool"))
     func discoveredLibtoolSpecInfo() async throws {
         try await withSpec(LibtoolLinkerSpec.self, .deferred) { (info: DiscoveredLibtoolLinkerToolSpecInfo) in
             #expect(info.toolPath.basename == "libtool")
diff --git a/Tests/SWBCoreTests/FileTextEncodingTests.swift b/Tests/SWBCoreTests/FileTextEncodingTests.swift
index 190dca66..ee898097 100644
--- a/Tests/SWBCoreTests/FileTextEncodingTests.swift
+++ b/Tests/SWBCoreTests/FileTextEncodingTests.swift
@@ -26,8 +26,7 @@ import SWBTestSupport
         #expect(FileTextEncoding("utf8") != FileTextEncoding.utf8)
     }
 
-    @Test(.skipHostOS(.windows, "feature not available on Windows due to missing CF APIs"),
-          .skipHostOS(.linux, "feature not available on Linux due to missing CF APIs"))
+    @Test(.requireHostOS(.macOS)) // requires CoreFoundation which is macOS-only
     func encoding() throws {
         #expect(FileTextEncoding.utf8.stringEncoding == String.Encoding.utf8)
         #expect(FileTextEncoding.utf16.stringEncoding == String.Encoding.utf16)
diff --git a/Tests/SWBCoreTests/SettingsTests.swift b/Tests/SWBCoreTests/SettingsTests.swift
index fc9f0ef0..be9080a1 100644
--- a/Tests/SWBCoreTests/SettingsTests.swift
+++ b/Tests/SWBCoreTests/SettingsTests.swift
@@ -1772,7 +1772,7 @@ import SWBMacro
         #expect(!core.platformRegistry.platforms.isEmpty)
         for developmentTeam in ["ABCDWXYZ", ""] {
             for platform in core.platformRegistry.platforms {
-                if ["android", "linux", "qnx", "windows"].contains(platform.name) {
+                if ["android", "freebsd", "linux", "qnx", "windows"].contains(platform.name) {
                     continue
                 }
                 for sdk in platform.sdks {
diff --git a/Tests/SWBTaskExecutionTests/PBXCpTests.swift b/Tests/SWBTaskExecutionTests/PBXCpTests.swift
index 109bba4d..cfb37a75 100644
--- a/Tests/SWBTaskExecutionTests/PBXCpTests.swift
+++ b/Tests/SWBTaskExecutionTests/PBXCpTests.swift
@@ -553,7 +553,7 @@ fileprivate struct PBXCpTests: CoreBasedTests {
         }
     }
 
-    @Test
+    @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
     func skipCopyIfContentsEqual() async throws {
         try await withTemporaryDirectory { tmp in
             let src = tmp.join("src")
diff --git a/Tests/SWBUtilTests/ElapsedTimerTests.swift b/Tests/SWBUtilTests/ElapsedTimerTests.swift
index e9f6dab6..3c364148 100644
--- a/Tests/SWBUtilTests/ElapsedTimerTests.swift
+++ b/Tests/SWBUtilTests/ElapsedTimerTests.swift
@@ -13,9 +13,10 @@
 import Foundation
 import SWBUtil
 import Testing
+import SWBTestSupport
 
 @Suite fileprivate struct ElapsedTimerTests {
-    @Test
+    @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
     func time() async throws {
         do {
             let delta = try await ElapsedTimer.measure {
diff --git a/Tests/SWBUtilTests/FSProxyTests.swift b/Tests/SWBUtilTests/FSProxyTests.swift
index 557a33d1..1f56717d 100644
--- a/Tests/SWBUtilTests/FSProxyTests.swift
+++ b/Tests/SWBUtilTests/FSProxyTests.swift
@@ -488,7 +488,7 @@ import SWBTestSupport
             case .android, .linux:
                 // This will _usually_ be correct on Linux-derived OSes (see above), but not always.
                 #expect(current_gid == ownership.group)
-            case .macOS, .iOS, .tvOS, .watchOS, .visionOS:
+            case .macOS, .iOS, .tvOS, .watchOS, .visionOS, .freebsd, .openbsd:
                 #expect(parentDirOwnership.group == ownership.group)
             case .windows:
                 // POSIX permissions don't exist, so everything is hardcoded to zero.
@@ -566,7 +566,7 @@ import SWBTestSupport
         }
     }
 
-    @Test(.skipHostOS(.windows))
+    @Test(.skipHostOS(.windows), .skipHostOS(.freebsd, "Blocked on https://github.com/swiftlang/swift/pull/77836"))
     func extendedAttributesSupport() throws {
         try withTemporaryDirectory { (tmpDir: Path) in
             // Many filesystems on other platforms (e.g. various non-ext4 temporary filesystems on Linux) don't support xattrs and will return ENOTSUP.
diff --git a/Tests/SWBUtilTests/FileHandleTests.swift b/Tests/SWBUtilTests/FileHandleTests.swift
index e118f507..51b504b3 100644
--- a/Tests/SWBUtilTests/FileHandleTests.swift
+++ b/Tests/SWBUtilTests/FileHandleTests.swift
@@ -22,7 +22,7 @@ import SystemPackage
 #endif
 
 @Suite fileprivate struct FileHandleTests {
-    @Test
+    @Test(.skipHostOS(.freebsd, "Currently crashes on FreeBSD"))
     func asyncReadFileDescriptor() async throws {
         let fs = localFS
         try await withTemporaryDirectory(fs: fs) { testDataPath in
diff --git a/Tests/SWBUtilTests/HeavyCacheTests.swift b/Tests/SWBUtilTests/HeavyCacheTests.swift
index 49c496b4..e4f0c9fb 100644
--- a/Tests/SWBUtilTests/HeavyCacheTests.swift
+++ b/Tests/SWBUtilTests/HeavyCacheTests.swift
@@ -14,6 +14,7 @@ import Foundation
 import Testing
 @_spi(Testing) import SWBUtil
 import Synchronization
+import SWBTestSupport
 
 @Suite
 fileprivate struct HeavyCacheTests {
@@ -105,7 +106,7 @@ fileprivate struct HeavyCacheTests {
     }
 
     /// Check initial TTL.
-    @Test
+    @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
     func TTL_initial() async throws {
         let fudgeFactor = 10.0
         let ttl = Duration.seconds(0.01)
@@ -124,7 +125,7 @@ fileprivate struct HeavyCacheTests {
     }
 
     /// Check TTL set after the fact.
-    @Test
+    @Test(.skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
     func TTL_after() async throws {
         let fudgeFactor = 10.0
         let ttl = Duration.seconds(0.01)
diff --git a/Tests/SWBUtilTests/MiscTests.swift b/Tests/SWBUtilTests/MiscTests.swift
index 36240ad9..f93094a4 100644
--- a/Tests/SWBUtilTests/MiscTests.swift
+++ b/Tests/SWBUtilTests/MiscTests.swift
@@ -25,7 +25,7 @@ import SWBUtil
             #expect(SWBUtil.userCacheDir().str.hasPrefix("/var/folders"))
         case .android:
             #expect(SWBUtil.userCacheDir().str.hasPrefix("/data/local/tmp"))
-        case .linux, .unknown:
+        case .linux, .freebsd, .openbsd, .unknown:
             #expect(SWBUtil.userCacheDir().str.hasPrefix("/tmp"))
         }
     }
diff --git a/Tests/SWBUtilTests/RateLimiterTests.swift b/Tests/SWBUtilTests/RateLimiterTests.swift
index 932b94fb..3affb9b4 100644
--- a/Tests/SWBUtilTests/RateLimiterTests.swift
+++ b/Tests/SWBUtilTests/RateLimiterTests.swift
@@ -13,8 +13,10 @@
 import Foundation
 import Testing
 import SWBUtil
+import SWBTestSupport
 
-@Suite fileprivate struct RateLimiterTests {
+@Suite(.skipHostOS(.freebsd, "Currently hangs on FreeBSD"))
+fileprivate struct RateLimiterTests {
     @Test
     func rateLimiterSeconds() async throws {
         let timer = ElapsedTimer()

From b924b0e9cce78e34dcfc2517df5fa745042e2a2f Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 15 Jul 2025 08:39:57 -0700
Subject: [PATCH 36/49] Make Android NDK discovery more robust, and add tests

Add support for Android NDK versions 23 through 26, and emit an explicit error for older versions. Add test coverage to verify parsing of the metadata.

I chose 23 as the cutoff for now simply because that's the next last time that the abis.json schema changed. That version was released in  August 2021. We can add older versions if anyone really wants them.
---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   | 231 +++++++----
 Sources/SWBAndroidPlatform/Plugin.swift       |  11 +-
 .../AndroidSDKTests.swift                     | 361 ++++++++++++++++++
 3 files changed, 523 insertions(+), 80 deletions(-)
 create mode 100644 Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift

diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index 44b93114..cb66619d 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -10,30 +10,51 @@
 //
 //===----------------------------------------------------------------------===//
 
-import SWBUtil
-import Foundation
+public import SWBUtil
+public import Foundation
 
-struct AndroidSDK: Sendable {
+@_spi(Testing) public struct AndroidSDK: Sendable {
     public let host: OperatingSystem
     public let path: Path
-    public let ndkVersion: Version?
+
+    /// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
+    @_spi(Testing) public let ndks: [NDK]
+
+    public var latestNDK: NDK? {
+        ndks.last
+    }
 
     init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
         self.host = host
         self.path = path
+        self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
+    }
 
-        let ndkBasePath = path.join("ndk")
-        if fs.exists(ndkBasePath) {
-            self.ndkVersion = try fs.listdir(ndkBasePath).map { try Version($0) }.max()
-        } else {
-            self.ndkVersion = nil
-        }
+    @_spi(Testing) public struct NDK: Equatable, Sendable {
+        public static let minimumNDKVersion = Version(23)
+
+        public let host: OperatingSystem
+        public let path: Path
+        public let version: Version
+        public let abis: [String: ABI]
+        public let deploymentTargetRange: DeploymentTargetRange
+
+        init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
+            self.host = host
+            self.path = ndkPath
+            self.version = version
 
-        if let ndkVersion {
-            let ndkPath = ndkBasePath.join(ndkVersion.description)
             let metaPath = ndkPath.join("meta")
 
-            self.abis = try JSONDecoder().decode([String: ABI].self, from: Data(fs.read(metaPath.join("abis.json"))))
+            guard #available(macOS 14, *) else {
+                throw StubError.error("Unsupported macOS version")
+            }
+
+            if version < Self.minimumNDKVersion {
+                throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
+            }
+
+            self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis
 
             struct PlatformsInfo: Codable {
                 let min: Int
@@ -41,87 +62,145 @@ struct AndroidSDK: Sendable {
             }
 
             let platformsInfo = try JSONDecoder().decode(PlatformsInfo.self, from: Data(fs.read(metaPath.join("platforms.json"))))
-            self.ndkPath = ndkPath
-            deploymentTargetRange = (platformsInfo.min, platformsInfo.max)
-        } else {
-            ndkPath = nil
-            deploymentTargetRange = nil
-            abis = nil
+            deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
         }
-    }
 
-    struct ABI: Codable {
-        enum Bitness: Int, Codable {
-            case bits32 = 32
-            case bits64 = 64
+        struct ABIs: DecodableWithConfiguration {
+            let abis: [String: ABI]
+
+            init(from decoder: any Decoder, configuration: Version) throws {
+                struct DynamicCodingKey: CodingKey {
+                    var stringValue: String
+                    
+                    init?(stringValue: String) {
+                        self.stringValue = stringValue
+                    }
+                    
+                    let intValue: Int? = nil
+
+                    init?(intValue: Int) {
+                        nil
+                    }
+                }
+                let container = try decoder.container(keyedBy: DynamicCodingKey.self)
+                abis = try Dictionary(uniqueKeysWithValues: container.allKeys.map { try ($0.stringValue, container.decode(ABI.self, forKey: $0, configuration: configuration)) })
+            }
         }
 
-        struct LLVMTriple: Codable {
-            var arch: String
-            var vendor: String
-            var system: String
-            var environment: String
-
-            var description: String {
-                "\(arch)-\(vendor)-\(system)-\(environment)"
+        @_spi(Testing) public struct ABI: DecodableWithConfiguration, Equatable, Sendable {
+            @_spi(Testing) public enum Bitness: Int, Codable, Equatable, Sendable {
+                case bits32 = 32
+                case bits64 = 64
             }
 
-            init(from decoder: any Decoder) throws {
-                let container = try decoder.singleValueContainer()
-                let triple = try container.decode(String.self)
-                if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
-                    self.arch = String(match.output.arch)
-                    self.vendor = String(match.output.vendor)
-                    self.system = String(match.output.system)
-                    self.environment = String(match.output.environment)
-                } else {
-                    throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
+            @_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable {
+                public var arch: String
+                public var vendor: String
+                public var system: String
+                public var environment: String
+
+                var description: String {
+                    "\(arch)-\(vendor)-\(system)-\(environment)"
+                }
+
+                public init(from decoder: any Decoder) throws {
+                    let container = try decoder.singleValueContainer()
+                    let triple = try container.decode(String.self)
+                    if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
+                        self.arch = String(match.output.arch)
+                        self.vendor = String(match.output.vendor)
+                        self.system = String(match.output.system)
+                        self.environment = String(match.output.environment)
+                    } else {
+                        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
+                    }
                 }
             }
 
-            func encode(to encoder: any Encoder) throws {
-                var container = encoder.singleValueContainer()
-                try container.encode(description)
+            public let bitness: Bitness
+            public let `default`: Bool
+            public let deprecated: Bool
+            public let proc: String
+            public let arch: String
+            public let triple: String
+            public let llvm_triple: LLVMTriple
+            public let min_os_version: Int
+
+            enum CodingKeys: String, CodingKey {
+                case bitness
+                case `default` = "default"
+                case deprecated
+                case proc
+                case arch
+                case triple
+                case llvm_triple = "llvm_triple"
+                case min_os_version = "min_os_version"
+            }
+
+            public init(from decoder: any Decoder, configuration ndkVersion: Version) throws {
+                let container = try decoder.container(keyedBy: CodingKeys.self)
+                self.bitness = try container.decode(Bitness.self, forKey: .bitness)
+                self.default = try container.decode(Bool.self, forKey: .default)
+                self.deprecated = try container.decode(Bool.self, forKey: .deprecated)
+                self.proc = try container.decode(String.self, forKey: .proc)
+                self.arch = try container.decode(String.self, forKey: .arch)
+                self.triple = try container.decode(String.self, forKey: .triple)
+                self.llvm_triple = try container.decode(LLVMTriple.self, forKey: .llvm_triple)
+                self.min_os_version = try container.decodeIfPresent(Int.self, forKey: .min_os_version) ?? {
+                    if ndkVersion < Version(27) {
+                        return 21 // min_os_version wasn't present prior to NDKr27, fill it in with 21, which is the appropriate value
+                    } else {
+                        throw DecodingError.valueNotFound(Int.self, .init(codingPath: container.codingPath, debugDescription: "No value associated with key \(CodingKeys.min_os_version) (\"\(CodingKeys.min_os_version.rawValue)\")."))
+                    }
+                }()
             }
         }
 
-        let bitness: Bitness
-        let `default`: Bool
-        let deprecated: Bool
-        let proc: String
-        let arch: String
-        let triple: String
-        let llvm_triple: LLVMTriple
-        let min_os_version: Int
-    }
+        @_spi(Testing) public struct DeploymentTargetRange: Equatable, Sendable {
+            public let min: Int
+            public let max: Int
+        }
 
-    public let abis: [String: ABI]?
+        public var toolchainPath: Path {
+            path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
+        }
 
-    public let deploymentTargetRange: (min: Int, max: Int)?
+        public var sysroot: Path {
+            toolchainPath.join("sysroot")
+        }
 
-    public let ndkPath: Path?
+        private var hostTag: String? {
+            switch host {
+            case .windows:
+                // Also works on Windows on ARM via Prism binary translation.
+                "windows-x86_64"
+            case .macOS:
+                // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
+                "darwin-x86_64"
+            case .linux:
+                // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
+                "linux-x86_64"
+            default:
+                nil // unsupported host
+            }
+        }
 
-    public var toolchainPath: Path? {
-        ndkPath?.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
-    }
+        public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
+            let ndkBasePath = sdkPath.join("ndk")
+            guard fs.exists(ndkBasePath) else {
+                return []
+            }
 
-    public var sysroot: Path? {
-        toolchainPath?.join("sysroot")
-    }
+            let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
+            let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }
 
-    private var hostTag: String? {
-        switch host {
-        case .windows:
-            // Also works on Windows on ARM via Prism binary translation.
-            "windows-x86_64"
-        case .macOS:
-            // Despite the x86_64 tag in the Darwin name, these are universal binaries including arm64.
-            "darwin-x86_64"
-        case .linux:
-            // Also works on non-x86 archs via binfmt support and qemu (or Rosetta on Apple-hosted VMs).
-            "linux-x86_64"
-        default:
-            nil // unsupported host
+            // If we have some NDKs but all of them are unsupported, try parsing them so that parsing fails and provides a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
+            let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks
+
+            return try discoveredNdks.map { ndkVersion in
+                let ndkPath = ndkBasePath.join(ndkVersion.description)
+                return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
+            }
         }
     }
 
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 8b6c8d5a..88fa5f76 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -54,7 +54,7 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
             if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
                 return [
                     "ANDROID_SDK_ROOT": latest.path.str,
-                    "ANDROID_NDK_ROOT": latest.ndkPath?.str,
+                    "ANDROID_NDK_ROOT": latest.ndks.last?.path.str,
                 ].compactMapValues { $0 }
             }
         default:
@@ -112,10 +112,13 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             return []
         }
 
-        guard let abis = androidSdk.abis, let deploymentTargetRange = androidSdk.deploymentTargetRange else {
+        guard let androidNdk = androidSdk.latestNDK else {
             return []
         }
 
+        let abis = androidNdk.abis
+        let deploymentTargetRange = androidNdk.deploymentTargetRange
+
         let allPossibleTriples = abis.values.flatMap { abi in
             (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
                 var triple = abi.llvm_triple
@@ -147,7 +150,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             swiftSettings = [:]
         }
 
-        return [(androidSdk.sysroot ?? .root, androidPlatform, [
+        return [(androidNdk.sysroot, androidPlatform, [
             "Type": .plString("SDK"),
             "Version": .plString("0.0.0"),
             "CanonicalName": .plString("android"),
@@ -184,7 +187,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
     let plugin: AndroidPlugin
 
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
-        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.toolchainPath else {
+        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else {
             return []
         }
 
diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
new file mode 100644
index 00000000..37329d94
--- /dev/null
+++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
@@ -0,0 +1,361 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+@_spi(Testing) import SWBAndroidPlatform
+import SWBTestSupport
+import SWBUtil
+import Testing
+
+@Suite
+fileprivate struct AndroidSDKTests {
+    @Test func findInstallations() async throws {
+        let host = try ProcessInfo.processInfo.hostOperatingSystem()
+        let installations = try await AndroidSDK.findInstallations(host: host, fs: localFS)
+        // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed
+        for installation in installations {
+            #expect(installation.host == host)
+            #expect(installation.latestNDK == installation.ndks.last)
+        }
+    }
+
+    @Test func abis_r22() async throws {
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in
+            let error = #expect(throws: StubError.self) {
+                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            }
+            #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)")
+        }
+    }
+
+    @Test func abis_r26_3() async throws {
+        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+                contents <<<
+                """
+                {
+                    "armeabi-v7a": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "armv7-a",
+                        "arch": "arm",
+                        "triple": "arm-linux-androideabi",
+                        "llvm_triple": "armv7-none-linux-androideabi"
+                    },
+                    "arm64-v8a": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "aarch64",
+                        "arch": "arm64",
+                        "triple": "aarch64-linux-android",
+                        "llvm_triple": "aarch64-none-linux-android"
+                    },
+                    "x86": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "i686",
+                        "arch": "x86",
+                        "triple": "i686-linux-android",
+                        "llvm_triple": "i686-none-linux-android"
+                    },
+                    "x86_64": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "x86_64",
+                        "arch": "x86_64",
+                        "triple": "x86_64-linux-android",
+                        "llvm_triple": "x86_64-none-linux-android"
+                    }
+                }
+                """
+            }
+
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+                contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 34,
+                    "aliases": {
+                        "20": 19,
+                        "25": 24,
+                        "J": 16,
+                        "J-MR1": 17,
+                        "J-MR2": 18,
+                        "K": 19,
+                        "L": 21,
+                        "L-MR1": 22,
+                        "M": 23,
+                        "N": 24,
+                        "N-MR1": 24,
+                        "O": 26,
+                        "O-MR1": 27,
+                        "P": 28,
+                        "Q": 29,
+                        "R": 30,
+                        "S": 31,
+                        "Sv2": 32,
+                        "Tiramisu": 33,
+                        "UpsideDownCake": 34
+                    }
+                }
+                """
+            }
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            let installation = try #require(installations.only)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPath)
+            #expect(try installation.version == Version("26.3.11579264"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 34)
+
+            #expect(installation.abis.count == 4)
+
+            let armv7 = try #require(installation.abis["armeabi-v7a"])
+            #expect(armv7.bitness == .bits32)
+            #expect(armv7.default == true)
+            #expect(armv7.deprecated == false)
+            #expect(armv7.proc == "armv7-a")
+            #expect(armv7.arch == "arm")
+            #expect(armv7.triple == "arm-linux-androideabi")
+            #expect(armv7.llvm_triple.arch == "armv7")
+            #expect(armv7.llvm_triple.vendor == "none")
+            #expect(armv7.llvm_triple.system == "linux")
+            #expect(armv7.llvm_triple.environment == "androideabi")
+            #expect(armv7.min_os_version == 21)
+
+            let arm64 = try #require(installation.abis["arm64-v8a"])
+            #expect(arm64.bitness == .bits64)
+            #expect(arm64.default == true)
+            #expect(arm64.deprecated == false)
+            #expect(arm64.proc == "aarch64")
+            #expect(arm64.arch == "arm64")
+            #expect(arm64.triple == "aarch64-linux-android")
+            #expect(arm64.llvm_triple.arch == "aarch64")
+            #expect(arm64.llvm_triple.vendor == "none")
+            #expect(arm64.llvm_triple.system == "linux")
+            #expect(arm64.llvm_triple.environment == "android")
+            #expect(arm64.min_os_version == 21)
+
+            let x86 = try #require(installation.abis["x86"])
+            #expect(x86.bitness == .bits32)
+            #expect(x86.default == true)
+            #expect(x86.deprecated == false)
+            #expect(x86.proc == "i686")
+            #expect(x86.arch == "x86")
+            #expect(x86.triple == "i686-linux-android")
+            #expect(x86.llvm_triple.arch == "i686")
+            #expect(x86.llvm_triple.vendor == "none")
+            #expect(x86.llvm_triple.system == "linux")
+            #expect(x86.llvm_triple.environment == "android")
+            #expect(x86.min_os_version == 21)
+
+            let x86_64 = try #require(installation.abis["x86_64"])
+            #expect(x86_64.bitness == .bits64)
+            #expect(x86_64.default == true)
+            #expect(x86_64.deprecated == false)
+            #expect(x86_64.proc == "x86_64")
+            #expect(x86_64.arch == "x86_64")
+            #expect(x86_64.triple == "x86_64-linux-android")
+            #expect(x86_64.llvm_triple.arch == "x86_64")
+            #expect(x86_64.llvm_triple.vendor == "none")
+            #expect(x86_64.llvm_triple.system == "linux")
+            #expect(x86_64.llvm_triple.environment == "android")
+            #expect(x86_64.min_os_version == 21)
+        }
+    }
+
+    @Test func abis_r27() async throws {
+        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+                contents <<<
+                """
+                {
+                    "armeabi-v7a": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "armv7-a",
+                        "arch": "arm",
+                        "triple": "arm-linux-androideabi",
+                        "llvm_triple": "armv7-none-linux-androideabi",
+                        "min_os_version": 21
+                    },
+                    "arm64-v8a": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "aarch64",
+                        "arch": "arm64",
+                        "triple": "aarch64-linux-android",
+                        "llvm_triple": "aarch64-none-linux-android",
+                        "min_os_version": 21
+                    },
+                    "riscv64": {
+                        "bitness": 64,
+                        "default": false,
+                        "deprecated": false,
+                        "proc": "riscv64",
+                        "arch": "riscv64",
+                        "triple": "riscv64-linux-android",
+                        "llvm_triple": "riscv64-none-linux-android",
+                        "min_os_version": 35
+                    },
+                    "x86": {
+                        "bitness": 32,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "i686",
+                        "arch": "x86",
+                        "triple": "i686-linux-android",
+                        "llvm_triple": "i686-none-linux-android",
+                        "min_os_version": 21
+                    },
+                    "x86_64": {
+                        "bitness": 64,
+                        "default": true,
+                        "deprecated": false,
+                        "proc": "x86_64",
+                        "arch": "x86_64",
+                        "triple": "x86_64-linux-android",
+                        "llvm_triple": "x86_64-none-linux-android",
+                        "min_os_version": 21
+                    }
+                }
+                """
+            }
+
+            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+                contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 35,
+                    "aliases": {
+                        "20": 19,
+                        "25": 24,
+                        "J": 16,
+                        "J-MR1": 17,
+                        "J-MR2": 18,
+                        "K": 19,
+                        "L": 21,
+                        "L-MR1": 22,
+                        "M": 23,
+                        "N": 24,
+                        "N-MR1": 24,
+                        "O": 26,
+                        "O-MR1": 27,
+                        "P": 28,
+                        "Q": 29,
+                        "R": 30,
+                        "S": 31,
+                        "Sv2": 32,
+                        "Tiramisu": 33,
+                        "UpsideDownCake": 34,
+                        "VanillaIceCream": 35
+                    }
+                }
+                """
+            }
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+            let installation = try #require(installations.only)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPath)
+            #expect(try installation.version == Version("27.0.11718014"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 35)
+
+            #expect(installation.abis.count == 5)
+
+            let armv7 = try #require(installation.abis["armeabi-v7a"])
+            #expect(armv7.bitness == .bits32)
+            #expect(armv7.default == true)
+            #expect(armv7.deprecated == false)
+            #expect(armv7.proc == "armv7-a")
+            #expect(armv7.arch == "arm")
+            #expect(armv7.triple == "arm-linux-androideabi")
+            #expect(armv7.llvm_triple.arch == "armv7")
+            #expect(armv7.llvm_triple.vendor == "none")
+            #expect(armv7.llvm_triple.system == "linux")
+            #expect(armv7.llvm_triple.environment == "androideabi")
+            #expect(armv7.min_os_version == 21)
+
+            let arm64 = try #require(installation.abis["arm64-v8a"])
+            #expect(arm64.bitness == .bits64)
+            #expect(arm64.default == true)
+            #expect(arm64.deprecated == false)
+            #expect(arm64.proc == "aarch64")
+            #expect(arm64.arch == "arm64")
+            #expect(arm64.triple == "aarch64-linux-android")
+            #expect(arm64.llvm_triple.arch == "aarch64")
+            #expect(arm64.llvm_triple.vendor == "none")
+            #expect(arm64.llvm_triple.system == "linux")
+            #expect(arm64.llvm_triple.environment == "android")
+            #expect(arm64.min_os_version == 21)
+
+            let riscv64 = try #require(installation.abis["riscv64"])
+            #expect(riscv64.bitness == .bits64)
+            #expect(riscv64.default == false)
+            #expect(riscv64.deprecated == false)
+            #expect(riscv64.proc == "riscv64")
+            #expect(riscv64.arch == "riscv64")
+            #expect(riscv64.triple == "riscv64-linux-android")
+            #expect(riscv64.llvm_triple.arch == "riscv64")
+            #expect(riscv64.llvm_triple.vendor == "none")
+            #expect(riscv64.llvm_triple.system == "linux")
+            #expect(riscv64.llvm_triple.environment == "android")
+            #expect(riscv64.min_os_version == 35)
+
+            let x86 = try #require(installation.abis["x86"])
+            #expect(x86.bitness == .bits32)
+            #expect(x86.default == true)
+            #expect(x86.deprecated == false)
+            #expect(x86.proc == "i686")
+            #expect(x86.arch == "x86")
+            #expect(x86.triple == "i686-linux-android")
+            #expect(x86.llvm_triple.arch == "i686")
+            #expect(x86.llvm_triple.vendor == "none")
+            #expect(x86.llvm_triple.system == "linux")
+            #expect(x86.llvm_triple.environment == "android")
+            #expect(x86.min_os_version == 21)
+
+            let x86_64 = try #require(installation.abis["x86_64"])
+            #expect(x86_64.bitness == .bits64)
+            #expect(x86_64.default == true)
+            #expect(x86_64.deprecated == false)
+            #expect(x86_64.proc == "x86_64")
+            #expect(x86_64.arch == "x86_64")
+            #expect(x86_64.triple == "x86_64-linux-android")
+            #expect(x86_64.llvm_triple.arch == "x86_64")
+            #expect(x86_64.llvm_triple.vendor == "none")
+            #expect(x86_64.llvm_triple.system == "linux")
+            #expect(x86_64.llvm_triple.environment == "android")
+            #expect(x86_64.min_os_version == 21)
+        }
+    }
+
+    private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws {
+        let fs = PseudoFS()
+        let ndkPath = Path.root.join("ndk")
+        let ndkVersionPath = ndkPath.join(version.description)
+        try fs.createDirectory(ndkPath, recursive: true)
+        try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+        let host = try ProcessInfo.processInfo.hostOperatingSystem()
+        try await block(host, fs, ndkVersionPath)
+    }
+}

From 004b5f827737831fdc0aaf46dace254ebf90f8be Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Fri, 1 Aug 2025 18:26:46 -0700
Subject: [PATCH 37/49] Improve Android SDK/NDK discovery

Now respects the environment variable overrides and looks for the Debian/Ubuntu package location.

Closes #495
---
 .github/scripts/linux_pre_build.sh            |  66 +++++++
 .github/workflows/pull_request.yml            |  36 +---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   | 187 ++++++++++++++----
 Sources/SWBAndroidPlatform/CMakeLists.txt     |   1 +
 .../SWBAndroidPlatform/JavaProperties.swift   |  32 +++
 Sources/SWBAndroidPlatform/Plugin.swift       |  44 +++--
 Sources/SWBUtil/FSProxy.swift                 |  10 +-
 Sources/SWBUtil/Path.swift                    |   4 +
 Sources/SWBUtil/URL.swift                     |  12 +-
 .../AndroidSDKTests.swift                     | 111 ++++++++---
 .../SWBAndroidPlatformTests.swift             |   7 +
 11 files changed, 392 insertions(+), 118 deletions(-)
 create mode 100755 .github/scripts/linux_pre_build.sh
 create mode 100644 Sources/SWBAndroidPlatform/JavaProperties.swift

diff --git a/.github/scripts/linux_pre_build.sh b/.github/scripts/linux_pre_build.sh
new file mode 100755
index 00000000..8adf2410
--- /dev/null
+++ b/.github/scripts/linux_pre_build.sh
@@ -0,0 +1,66 @@
+#!/bin/bash
+##===----------------------------------------------------------------------===##
+##
+## This source file is part of the Swift open source project
+##
+## Copyright (c) 2025 Apple Inc. and the Swift project authors
+## Licensed under Apache License v2.0 with Runtime Library Exception
+##
+## See http://swift.org/LICENSE.txt for license information
+## See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+##
+##===----------------------------------------------------------------------===##
+
+set -e
+
+if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
+    export DEBIAN_FRONTEND=noninteractive
+
+    apt-get update -y
+
+    # Build dependencies
+    apt-get install -y libsqlite3-dev libncurses-dev
+
+    # Debug symbols
+    apt-get install -y libc6-dbg
+
+    if [[ "$INSTALL_CMAKE" == "1" ]] ; then
+        apt-get install -y cmake ninja-build
+    fi
+
+    # Android NDK
+    dpkg_architecture="$(dpkg --print-architecture)"
+    if [[ "$SKIP_ANDROID" != "1" ]] && [[ "$dpkg_architecture" == amd64 ]] ; then
+        eval "$(cat /etc/lsb-release)"
+        case "$DISTRIB_CODENAME" in
+            bookworm|jammy)
+                : # Not available
+                ;;
+            noble)
+                apt-get install -y google-android-ndk-r26c-installer
+                ;;
+            *)
+                echo "Unknown distribution: $DISTRIB_CODENAME" >&2
+                exit 1
+        esac
+    else
+        echo "Skipping Android NDK installation on $dpkg_architecture" >&2
+    fi
+elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
+    dnf update -y
+
+    # Build dependencies
+    dnf install -y sqlite-devel ncurses-devel
+
+    # Debug symbols
+    dnf debuginfo-install -y glibc
+elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
+    yum update -y
+
+    # Build dependencies
+    yum install -y sqlite-devel ncurses-devel
+
+    # Debug symbols
+    yum install -y yum-utils
+    debuginfo-install -y glibc
+fi
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index c39112b4..1f1e70d1 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -14,33 +14,7 @@ jobs:
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
     with:
       linux_os_versions: '["noble", "jammy", "rhel-ubi9"]'
-      linux_pre_build_command: |
-        if command -v apt-get >/dev/null 2>&1 ; then # bookworm, noble, jammy
-          apt-get update -y
-
-          # Build dependencies
-          apt-get install -y libsqlite3-dev libncurses-dev
-
-          # Debug symbols
-          apt-get install -y libc6-dbg
-        elif command -v dnf >/dev/null 2>&1 ; then # rhel-ubi9
-          dnf update -y
-
-          # Build dependencies
-          dnf install -y sqlite-devel ncurses-devel
-
-          # Debug symbols
-          dnf debuginfo-install -y glibc
-        elif command -v yum >/dev/null 2>&1 ; then # amazonlinux2
-          yum update -y
-
-          # Build dependencies
-          yum install -y sqlite-devel ncurses-devel
-
-          # Debug symbols
-          yum install -y yum-utils
-          debuginfo-install -y glibc
-        fi
+      linux_pre_build_command: ./.github/scripts/linux_pre_build.sh
       linux_build_command: 'swift test --no-parallel'
       linux_swift_versions: '["nightly-main", "nightly-6.2"]'
       windows_swift_versions: '["nightly-main"]'
@@ -50,13 +24,7 @@ jobs:
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
     with:
       linux_os_versions: '["noble"]'
-      linux_pre_build_command: |
-        apt-get update -y
-
-        # Build dependencies
-        apt-get install -y libsqlite3-dev libncurses-dev
-
-        apt-get install -y cmake ninja-build
+      linux_pre_build_command: SKIP_ANDROID=1 INSTALL_CMAKE=1 ./.github/scripts/linux_pre_build.sh
       linux_build_command: 'swift package -Xbuild-tools-swiftc -DUSE_PROCESS_SPAWNING_WORKAROUND cmake-smoke-test --disable-sandbox --cmake-path `which cmake` --ninja-path `which ninja` --extra-cmake-arg -DCMAKE_C_COMPILER=`which clang` --extra-cmake-arg -DCMAKE_CXX_COMPILER=`which clang++` --extra-cmake-arg -DCMAKE_Swift_COMPILER=`which swiftc`'
       linux_swift_versions: '["nightly-main"]'
       windows_swift_versions: '[]'
diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index cb66619d..3b934838 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -15,43 +15,54 @@ public import Foundation
 
 @_spi(Testing) public struct AndroidSDK: Sendable {
     public let host: OperatingSystem
-    public let path: Path
+    public let path: AbsolutePath
+    private let ndkInstallations: NDK.Installations
 
     /// List of NDKs available in this SDK installation, sorted by version number from oldest to newest.
-    @_spi(Testing) public let ndks: [NDK]
+    @_spi(Testing) public var ndks: [NDK] {
+        ndkInstallations.ndks
+    }
 
-    public var latestNDK: NDK? {
-        ndks.last
+    public var preferredNDK: NDK? {
+        ndkInstallations.preferredNDK ?? ndks.last
     }
 
-    init(host: OperatingSystem, path: Path, fs: any FSProxy) throws {
+    init(host: OperatingSystem, path: AbsolutePath, fs: any FSProxy) throws {
         self.host = host
         self.path = path
-        self.ndks = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
+        self.ndkInstallations = try NDK.findInstallations(host: host, sdkPath: path, fs: fs)
     }
 
     @_spi(Testing) public struct NDK: Equatable, Sendable {
         public static let minimumNDKVersion = Version(23)
 
         public let host: OperatingSystem
-        public let path: Path
+        public let path: AbsolutePath
         public let version: Version
         public let abis: [String: ABI]
         public let deploymentTargetRange: DeploymentTargetRange
 
-        init(host: OperatingSystem, path ndkPath: Path, version: Version, fs: any FSProxy) throws {
+        @_spi(Testing) public init(host: OperatingSystem, path ndkPath: AbsolutePath, fs: any FSProxy) throws {
             self.host = host
             self.path = ndkPath
-            self.version = version
+            self.toolchainPath = try AbsolutePath(validating: path.path.join("toolchains").join("llvm").join("prebuilt").join(Self.hostTag(host)))
+            self.sysroot = try AbsolutePath(validating: toolchainPath.path.join("sysroot"))
+
+            let propertiesFile = ndkPath.path.join("source.properties")
+            guard fs.exists(propertiesFile) else {
+                throw Error.notAnNDK(ndkPath)
+            }
 
-            let metaPath = ndkPath.join("meta")
+            self.version = try NDK.Properties(data: Data(fs.read(propertiesFile))).revision
+
+            let metaPath = ndkPath.path.join("meta")
 
             guard #available(macOS 14, *) else {
                 throw StubError.error("Unsupported macOS version")
             }
 
             if version < Self.minimumNDKVersion {
-                throw StubError.error("Android NDK version at path '\(ndkPath.str)' is not supported (r\(Self.minimumNDKVersion.description) or later required)")
+                throw Error.unsupportedVersion(path: ndkPath, minimumVersion: Self.minimumNDKVersion)
             }
 
             self.abis = try JSONDecoder().decode(ABIs.self, from: Data(fs.read(metaPath.join("abis.json"))), configuration: version).abis
@@ -65,6 +76,36 @@ public import Foundation
             deploymentTargetRange = DeploymentTargetRange(min: platformsInfo.min, max: platformsInfo.max)
         }
 
+        public enum Error: Swift.Error, CustomStringConvertible, Sendable {
+            case notAnNDK(AbsolutePath)
+            case unsupportedVersion(path: AbsolutePath, minimumVersion: Version)
+            case noSupportedVersions(minimumVersion: Version)
+
+            public var description: String {
+                switch self {
+                case let .notAnNDK(path):
+                    "Package at path '\(path.path.str)' is not an Android NDK (no source.properties file)"
+                case let .unsupportedVersion(path, minimumVersion):
+                    "Android NDK version at path '\(path.path.str)' is not supported (r\(minimumVersion.description) or later required)"
+                case let .noSupportedVersions(minimumVersion):
+                    "All installed NDK versions are not supported (r\(minimumVersion.description) or later required)"
+                }
+            }
+        }
+
+        struct Properties {
+            let properties: JavaProperties
+            let revision: Version
+
+            init(data: Data) throws {
+                properties = try .init(data: data)
+                guard properties["Pkg.Desc"] == "Android NDK" else {
+                    throw StubError.error("Package is not an Android NDK")
+                }
+                revision = try Version(properties["Pkg.BaseRevision"] ?? properties["Pkg.Revision"] ?? "")
+            }
+        }
+
         struct ABIs: DecodableWithConfiguration {
             let abis: [String: ABI]
 
@@ -161,15 +202,10 @@ public import Foundation
             public let max: Int
         }
 
-        public var toolchainPath: Path {
-            path.join("toolchains").join("llvm").join("prebuilt").join(hostTag)
-        }
-
-        public var sysroot: Path {
-            toolchainPath.join("sysroot")
-        }
+        public let toolchainPath: AbsolutePath
+        public let sysroot: AbsolutePath
 
-        private var hostTag: String? {
+        private static func hostTag(_ host: OperatingSystem) -> String? {
             switch host {
             case .windows:
                 // Also works on Windows on ARM via Prism binary translation.
@@ -185,44 +221,119 @@ public import Foundation
             }
         }
 
-        public static func findInstallations(host: OperatingSystem, sdkPath: Path, fs: any FSProxy) throws -> [NDK] {
-            let ndkBasePath = sdkPath.join("ndk")
+        public struct Installations: Sendable {
+            private let preferredIndex: Int?
+            public let ndks: [NDK]
+
+            init(preferredIndex: Int? = nil, ndks: [NDK]) {
+                self.preferredIndex = preferredIndex
+                self.ndks = ndks
+            }
+
+            public var preferredNDK: NDK? {
+                preferredIndex.map { ndks[$0] } ?? ndks.only
+            }
+        }
+
+        public static func findInstallations(host: OperatingSystem, sdkPath: AbsolutePath, fs: any FSProxy) throws -> Installations {
+            if let overridePath = NDK.environmentOverrideLocation {
+                return try Installations(ndks: [NDK(host: host, path: overridePath, fs: fs)])
+            }
+
+            let ndkBasePath = sdkPath.path.join("ndk")
             guard fs.exists(ndkBasePath) else {
-                return []
+                return Installations(ndks: [])
             }
 
-            let ndks = try fs.listdir(ndkBasePath).map({ try Version($0) }).sorted()
-            let supportedNdks = ndks.filter { $0 >= minimumNDKVersion }
+            var hadUnsupportedVersions: Bool = false
+            let ndks = try fs.listdir(ndkBasePath).compactMap({ subdir in
+                do {
+                    return try NDK(host: host, path: AbsolutePath(validating: ndkBasePath.join(subdir)), fs: fs)
+                } catch Error.notAnNDK(_) {
+                    return nil
+                } catch Error.unsupportedVersion(_, _) {
+                    hadUnsupportedVersions = true
+                    return nil
+                }
+            }).sorted(by: \.version)
 
-            // If we have some NDKs but all of them are unsupported, try parsing them so that parsing fails and provides a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
-            let discoveredNdks = supportedNdks.isEmpty && !ndks.isEmpty ? ndks : supportedNdks
+            // If we have some NDKs but all of them are unsupported, provide a more useful error. Otherwise, simply filter out and ignore the unsupported versions.
+            if ndks.isEmpty && hadUnsupportedVersions {
+                throw Error.noSupportedVersions(minimumVersion: Self.minimumNDKVersion)
+            }
 
-            return try discoveredNdks.map { ndkVersion in
-                let ndkPath = ndkBasePath.join(ndkVersion.description)
-                return try NDK(host: host, path: ndkPath, version: ndkVersion, fs: fs)
+            // Respect Debian alternatives
+            let preferredIndex: Int?
+            if sdkPath == AndroidSDK.defaultDebianLocation, let ndkLinkPath = AndroidSDK.NDK.defaultDebianLocation {
+                preferredIndex = try ndks.firstIndex(where: { try $0.path.path == fs.realpath(ndkLinkPath.path) })
+            } else {
+                preferredIndex = nil
             }
+
+            return Installations(preferredIndex: preferredIndex, ndks: ndks)
         }
     }
 
     public static func findInstallations(host: OperatingSystem, fs: any FSProxy) async throws -> [AndroidSDK] {
-        let defaultLocation: Path? = switch host {
+        var paths: [AbsolutePath] = []
+        if let path = AndroidSDK.environmentOverrideLocation {
+            paths.append(path)
+        }
+        if let path = try AndroidSDK.defaultAndroidStudioLocation(host: host) {
+            paths.append(path)
+        }
+        if let path = AndroidSDK.defaultDebianLocation, host == .linux {
+            paths.append(path)
+        }
+        return try paths.compactMap { path in
+            guard fs.exists(path.path) else {
+                return nil
+            }
+            return try AndroidSDK(host: host, path: path, fs: fs)
+        }
+    }
+}
+
+fileprivate extension AndroidSDK.NDK {
+    /// The location of the Android NDK based on the `ANDROID_NDK_ROOT` environment variable (falling back to the deprecated but well known `ANDROID_NDK_HOME`).
+    /// - seealso: [Configuring NDK Path](https://github.com/android/ndk-samples/wiki/Configure-NDK-Path#terminologies)
+    static var environmentOverrideLocation: AbsolutePath? {
+        (getEnvironmentVariable("ANDROID_NDK_ROOT") ?? getEnvironmentVariable("ANDROID_NDK_HOME"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
+    }
+
+    /// Location of the Android NDK installed by the `google-android-ndk-*-installer` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
+    /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
+    static var defaultDebianLocation: AbsolutePath? {
+        AbsolutePath("/usr/lib/android-ndk")
+    }
+}
+
+fileprivate extension AndroidSDK {
+    /// The location of the Android SDK based on the `ANDROID_HOME` environment variable (falling back to the deprecated but well known `ANDROID_SDK_ROOT`).
+    /// - seealso: [Android environment variables](https://developer.android.com/tools/variables)
+    static var environmentOverrideLocation: AbsolutePath? {
+        (getEnvironmentVariable("ANDROID_HOME") ?? getEnvironmentVariable("ANDROID_SDK_ROOT"))?.nilIfEmpty.map { AbsolutePath($0) } ?? nil
+    }
+
+    static func defaultAndroidStudioLocation(host: OperatingSystem) throws -> AbsolutePath? {
+        switch host {
         case .windows:
             // %LOCALAPPDATA%\Android\Sdk
-            try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").filePath
+            try FileManager.default.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("Sdk").absoluteFilePath
         case .macOS:
             // ~/Library/Android/sdk
-            try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").filePath
+            try FileManager.default.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: false).appendingPathComponent("Android").appendingPathComponent("sdk").absoluteFilePath
         case .linux:
             // ~/Android/Sdk
-            Path.homeDirectory.join("Android").join("Sdk")
+            try AbsolutePath(validating: Path.homeDirectory.join("Android").join("Sdk"))
         default:
             nil
         }
+    }
 
-        if let path = defaultLocation, fs.exists(path) {
-            return try [AndroidSDK(host: host, path: path, fs: fs)]
-        }
-
-        return []
+    /// Location of the Android SDK installed by the `google-*` family of packages available in Debian 13 "Trixie" and Ubuntu 24.04 "Noble".
+    /// These packages are available in non-free / multiverse and multiple versions can be installed simultaneously.
+    static var defaultDebianLocation: AbsolutePath? {
+        AbsolutePath("/usr/lib/android-sdk")
     }
 }
diff --git a/Sources/SWBAndroidPlatform/CMakeLists.txt b/Sources/SWBAndroidPlatform/CMakeLists.txt
index fdb0bfa7..72d8cd9b 100644
--- a/Sources/SWBAndroidPlatform/CMakeLists.txt
+++ b/Sources/SWBAndroidPlatform/CMakeLists.txt
@@ -10,6 +10,7 @@ See http://swift.org/CONTRIBUTORS.txt for Swift project authors
 
 add_library(SWBAndroidPlatform
   AndroidSDK.swift
+  JavaProperties.swift
   Plugin.swift)
 SwiftBuild_Bundle(MODULE SWBAndroidPlatform FILES
   Specs/Android.xcspec)
diff --git a/Sources/SWBAndroidPlatform/JavaProperties.swift b/Sources/SWBAndroidPlatform/JavaProperties.swift
new file mode 100644
index 00000000..0a6059d7
--- /dev/null
+++ b/Sources/SWBAndroidPlatform/JavaProperties.swift
@@ -0,0 +1,32 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+import Foundation
+internal import SWBUtil
+
+/// A simple representation of a Java properties file.
+///
+/// See `java.util.Properties` for a description of the file format. This parser is a simplified version that doesn't handle line continuations, etc., because our use case is narrow.
+struct JavaProperties {
+    private let properties: [String: String]
+
+    init(data: Data) throws {
+        properties = Dictionary(uniqueKeysWithValues: String(decoding: data, as: UTF8.self).split(whereSeparator: { $0.isNewline }).map(String.init).map {
+            let (key, value) = $0.split("=")
+            return (key.trimmingCharacters(in: .whitespaces), value.trimmingCharacters(in: .whitespaces))
+        })
+    }
+
+    subscript(_ propertyName: String) -> String? {
+        properties[propertyName]
+    }
+}
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 88fa5f76..3eb0ca1d 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -11,7 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 public import SWBUtil
-import SWBCore
+public import SWBCore
 import SWBMacro
 import Foundation
 
@@ -24,7 +24,7 @@ import Foundation
     manager.register(AndroidToolchainRegistryExtension(plugin: plugin), type: ToolchainRegistryExtensionPoint.self)
 }
 
-final class AndroidPlugin: Sendable {
+@_spi(Testing) public final class AndroidPlugin: Sendable {
     private let androidSDKInstallations = AsyncCache<OperatingSystem, [AndroidSDK]>()
 
     func cachedAndroidSDKInstallations(host: OperatingSystem) async throws -> [AndroidSDK] {
@@ -33,6 +33,18 @@ final class AndroidPlugin: Sendable {
             try await AndroidSDK.findInstallations(host: host, fs: localFS)
         }
     }
+
+    @_spi(Testing) public func effectiveInstallation(host: OperatingSystem) async throws -> (sdk: AndroidSDK, ndk: AndroidSDK.NDK)? {
+        guard let androidSdk = try? await cachedAndroidSDKInstallations(host: host).first else {
+            return nil
+        }
+
+        guard let androidNdk = androidSdk.preferredNDK else {
+            return nil
+        }
+
+        return (androidSdk, androidNdk)
+    }
 }
 
 struct AndroidPlatformSpecsExtension: SpecificationsExtension {
@@ -52,9 +64,13 @@ struct AndroidEnvironmentExtension: EnvironmentExtension {
         switch context.hostOperatingSystem {
         case .windows, .macOS, .linux:
             if let latest = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first {
+                let sdkPath = latest.path.path.str
+                let ndkPath = latest.preferredNDK?.path.path.str
                 return [
-                    "ANDROID_SDK_ROOT": latest.path.str,
-                    "ANDROID_NDK_ROOT": latest.ndks.last?.path.str,
+                    "ANDROID_HOME": sdkPath,
+                    "ANDROID_SDK_ROOT": sdkPath,
+                    "ANDROID_NDK_ROOT": ndkPath,
+                    "ANDROID_NDK_HOME": ndkPath,
                 ].compactMapValues { $0 }
             }
         default:
@@ -80,10 +96,10 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
     }
 }
 
-struct AndroidSDKRegistryExtension: SDKRegistryExtension {
-    let plugin: AndroidPlugin
+@_spi(Testing) public struct AndroidSDKRegistryExtension: SDKRegistryExtension {
+    @_spi(Testing) public let plugin: AndroidPlugin
 
-    func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
+    public func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
         let host = context.hostOperatingSystem
         guard let androidPlatform = context.platformRegistry.lookup(name: "android") else {
             return []
@@ -108,11 +124,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             "AR": .plString(host.imageFormat.executableName(basename: "llvm-ar")),
         ]
 
-        guard let androidSdk = try? await plugin.cachedAndroidSDKInstallations(host: host).first else {
-            return []
-        }
-
-        guard let androidNdk = androidSdk.latestNDK else {
+        guard let (_, androidNdk) = try await plugin.effectiveInstallation(host: host) else {
             return []
         }
 
@@ -150,7 +162,7 @@ struct AndroidSDKRegistryExtension: SDKRegistryExtension {
             swiftSettings = [:]
         }
 
-        return [(androidNdk.sysroot, androidPlatform, [
+        return [(androidNdk.sysroot.path, androidPlatform, [
             "Type": .plString("SDK"),
             "Version": .plString("0.0.0"),
             "CanonicalName": .plString("android"),
@@ -187,7 +199,7 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
     let plugin: AndroidPlugin
 
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
-        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.latestNDK?.toolchainPath else {
+        guard let toolchainPath = try? await plugin.cachedAndroidSDKInstallations(host: context.hostOperatingSystem).first?.preferredNDK?.toolchainPath else {
             return []
         }
 
@@ -197,13 +209,13 @@ struct AndroidToolchainRegistryExtension: ToolchainRegistryExtension {
                 displayName: "Android",
                 version: Version(0, 0, 0),
                 aliases: [],
-                path: toolchainPath,
+                path: toolchainPath.path,
                 frameworkPaths: [],
                 libraryPaths: [],
                 defaultSettings: [:],
                 overrideSettings: [:],
                 defaultSettingsWhenPrimary: [:],
-                executableSearchPaths: [toolchainPath.join("bin")],
+                executableSearchPaths: [toolchainPath.path.join("bin")],
                 testingLibraryPlatformNames: [],
                 fs: context.fs)
         ]
diff --git a/Sources/SWBUtil/FSProxy.swift b/Sources/SWBUtil/FSProxy.swift
index 9f0eb4c2..2bc1e10e 100644
--- a/Sources/SWBUtil/FSProxy.swift
+++ b/Sources/SWBUtil/FSProxy.swift
@@ -961,9 +961,13 @@ public class PseudoFS: FSProxy, @unchecked Sendable {
 
     public func realpath(_ path: Path) throws -> Path {
         // TODO: Update this to actually return the link target when we support
-        // symlinks; for now it just returns the input, which seems reasonably
-        // correct.
-        return path
+        // symlinks; for now it just returns the input (or the link target if it's a symlink),
+        // which seems reasonably correct for simple cases.
+        do {
+            return try readlink(path)
+        } catch {
+            return path
+        }
     }
 
     public func readlink(_ path: Path) throws -> Path {
diff --git a/Sources/SWBUtil/Path.swift b/Sources/SWBUtil/Path.swift
index 1edf33b7..28294f29 100644
--- a/Sources/SWBUtil/Path.swift
+++ b/Sources/SWBUtil/Path.swift
@@ -1006,6 +1006,10 @@ public struct RelativePath: Hashable, Equatable, Serializable, Sendable {
 }
 
 extension AbsolutePath {
+    public static var root: AbsolutePath {
+        AbsolutePath(.root)!
+    }
+
     public var dirname: AbsolutePath {
         AbsolutePath(path.dirname)!
     }
diff --git a/Sources/SWBUtil/URL.swift b/Sources/SWBUtil/URL.swift
index 725f7ac5..9195baa6 100644
--- a/Sources/SWBUtil/URL.swift
+++ b/Sources/SWBUtil/URL.swift
@@ -18,7 +18,7 @@ extension URL {
     /// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash.
     ///
     /// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable.
-    public var filePath: Path {
+    public var absoluteFilePath: AbsolutePath {
         get throws {
             guard isFileURL else {
                 throw FileURLError.notRepresentable(self)
@@ -27,12 +27,16 @@ extension URL {
                 guard let cString else {
                     throw FileURLError.notRepresentable(self)
                 }
-                let fp = Path(String(cString: cString))
-                precondition(fp.isAbsolute, "path '\(fp.str)' is not absolute")
-                return fp
+                return try AbsolutePath(validating: String(cString: cString))
             }
         }
     }
+
+    public var filePath: Path {
+        get throws {
+            try absoluteFilePath.path
+        }
+    }
 }
 
 fileprivate enum FileURLError: Error, CustomStringConvertible {
diff --git a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
index 37329d94..b567c294 100644
--- a/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
+++ b/Tests/SWBAndroidPlatformTests/AndroidSDKTests.swift
@@ -24,22 +24,74 @@ fileprivate struct AndroidSDKTests {
         // It's OK if `installations` is an empty set, the host system might have no Android SDK/NDK installed
         for installation in installations {
             #expect(installation.host == host)
-            #expect(installation.latestNDK == installation.ndks.last)
         }
     }
 
-    @Test func abis_r22() async throws {
-        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, ndkVersionPath in
-            let error = #expect(throws: StubError.self) {
-                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
+    @Test(.skipHostOS(.windows, "This test inherently relies on Unix-style paths"))
+    func debian() async throws {
+        let fs = PseudoFS()
+        let sdkPath = try AbsolutePath(validating: "/usr/lib/android-sdk")
+        try fs.createDirectory(sdkPath.path, recursive: true)
+        try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: Version("24"), Version("25"), Version("26")) { host, fs, sdkPath, ndkVersionPaths in
+            for ndkVersionPath in ndkVersionPaths {
+                try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
+                    contents <<<
+                """
+                {
+                }
+                """
+                }
+
+                try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
+                    contents <<<
+                """
+                {
+                    "min": 21,
+                    "max": 35,
+                    "aliases": {
+                    }
+                }
+                """
+                }
             }
-            #expect(error?.description == "Android NDK version at path '\(ndkVersionPath.str)' is not supported (r23 or later required)")
+
+            try fs.symlink(Path("/usr/lib/android-ndk"), target: ndkVersionPaths[1].path)
+
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.preferredNDK)
+            #expect(installations.ndks.count == 3)
+            #expect(installation != installations.ndks.first)
+            #expect(installation == installations.ndks[1])
+            #expect(installation != installations.ndks.last)
+            #expect(installation.host == host)
+            #expect(installation.path == ndkVersionPaths[1])
+            #expect(try installation.version == Version("25"))
+            #expect(installation.deploymentTargetRange.min == 21)
+            #expect(installation.deploymentTargetRange.max == 35)
+
+            #expect(installation.abis.isEmpty)
+        }
+    }
+
+    @Test func unsupportedVersions() async throws {
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in
+            let error = try #require(throws: AndroidSDK.NDK.Error.self) {
+                try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            }
+            #expect(error.description == "All installed NDK versions are not supported (r23 or later required)")
+        }
+
+        try await withNDKVersion(version: Version("22.1.7171670")) { host, fs, sdkPath, ndkVersionPath in
+            let error = try #require(throws: AndroidSDK.NDK.Error.self) {
+                try AndroidSDK.NDK(host: host, path: ndkVersionPath, fs: fs)
+            }
+            #expect(error.description == "Android NDK version at path '\(ndkVersionPath.path.str)' is not supported (r23 or later required)")
         }
     }
 
     @Test func abis_r26_3() async throws {
-        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, ndkVersionPath in
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+        try await withNDKVersion(version: Version("26.3.11579264")) { host, fs, sdkPath, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
                 contents <<<
                 """
                 {
@@ -83,7 +135,7 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
                 contents <<<
                 """
                 {
@@ -115,8 +167,8 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
-            let installation = try #require(installations.only)
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.ndks.only)
             #expect(installation.host == host)
             #expect(installation.path == ndkVersionPath)
             #expect(try installation.version == Version("26.3.11579264"))
@@ -180,8 +232,8 @@ fileprivate struct AndroidSDKTests {
     }
 
     @Test func abis_r27() async throws {
-        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, ndkVersionPath in
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("abis.json")) { contents in
+        try await withNDKVersion(version: Version("27.0.11718014")) { host, fs, sdkPath, ndkVersionPath in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("abis.json")) { contents in
                 contents <<<
                 """
                 {
@@ -239,7 +291,7 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            try await fs.writeFileContents(ndkVersionPath.join("meta").join("platforms.json")) { contents in
+            try await fs.writeFileContents(ndkVersionPath.path.join("meta").join("platforms.json")) { contents in
                 contents <<<
                 """
                 {
@@ -272,8 +324,8 @@ fileprivate struct AndroidSDKTests {
                 """
             }
 
-            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: ndkVersionPath.dirname.dirname, fs: fs)
-            let installation = try #require(installations.only)
+            let installations = try AndroidSDK.NDK.findInstallations(host: host, sdkPath: sdkPath, fs: fs)
+            let installation = try #require(installations.ndks.only)
             #expect(installation.host == host)
             #expect(installation.path == ndkVersionPath)
             #expect(try installation.version == Version("27.0.11718014"))
@@ -349,13 +401,26 @@ fileprivate struct AndroidSDKTests {
         }
     }
 
-    private func withNDKVersion(version: Version, _ block: (OperatingSystem, any FSProxy, Path) async throws -> ()) async throws {
-        let fs = PseudoFS()
-        let ndkPath = Path.root.join("ndk")
-        let ndkVersionPath = ndkPath.join(version.description)
-        try fs.createDirectory(ndkPath, recursive: true)
-        try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+    private func withNDKVersions(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, versions: Version..., block: (OperatingSystem, any FSProxy, AbsolutePath, [AbsolutePath]) async throws -> ()) async throws {
+        let ndkPath = sdkPath.path.join("ndk")
+        let ndkVersionPaths = try await versions.asyncMap { version in
+            let ndkVersionPath = ndkPath.join(version.description)
+            try fs.createDirectory(ndkPath, recursive: true)
+            try fs.createDirectory(ndkVersionPath.join("meta"), recursive: true)
+            try await fs.writeFileContents(ndkVersionPath.join("source.properties")) {
+                $0 <<< "Pkg.Desc = Android NDK\n"
+                $0 <<< "Pkg.Revision = \(version.description)-beta1\n"
+                $0 <<< "Pkg.BaseRevision = \(version.description)\n"
+            }
+            return ndkVersionPath
+        }
         let host = try ProcessInfo.processInfo.hostOperatingSystem()
-        try await block(host, fs, ndkVersionPath)
+        try await block(host, fs, sdkPath, ndkVersionPaths.map { try AbsolutePath(validating: $0) })
+    }
+
+    private func withNDKVersion(fs: PseudoFS = PseudoFS(), sdkPath: AbsolutePath = .root, version: Version, _ block: (OperatingSystem, any FSProxy, AbsolutePath, AbsolutePath) async throws -> ()) async throws {
+        try await withNDKVersions(fs: fs, sdkPath: sdkPath, versions: version) { host, fs, sdkPath, ndkVersionPaths in
+            try await block(host, fs, sdkPath, ndkVersionPaths[0])
+        }
     }
 }
diff --git a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
index 74ef48e4..117588f9 100644
--- a/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
+++ b/Tests/SWBAndroidPlatformTests/SWBAndroidPlatformTests.swift
@@ -11,6 +11,7 @@
 //===----------------------------------------------------------------------===//
 
 import Testing
+@_spi(Testing) import SWBAndroidPlatform
 import SWBProtocol
 import SWBTestSupport
 import SWBTaskExecution
@@ -94,6 +95,12 @@ fileprivate struct AndroidBuildOperationTests: CoreBasedTests {
                     ),
                 ])
             let core = try await getCore()
+            let androidExtension = try await #require(core.pluginManager.extensions(of: SDKRegistryExtensionPoint.self).compactMap { $0 as? AndroidSDKRegistryExtension }.only)
+            let (_, androidNdk) = try #require(await androidExtension.plugin.effectiveInstallation(host: core.hostOperatingSystem))
+            if androidNdk.version < Version(27) && arch == "riscv64" {
+                return // riscv64 support was introduced in NDK r27
+            }
+
             let tester = try await BuildOperationTester(core, testProject, simulated: false)
 
             let projectDir = tester.workspace.projects[0].sourceRoot

From 6648241c8477ad6807bc8a35ff76154f5da831cb Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Sun, 15 Jun 2025 19:34:04 -0700
Subject: [PATCH 38/49] Lookup unix fallback toolchain relative to SWIFT_EXEC
 when set

---
 Sources/SWBGenericUnixPlatform/Plugin.swift | 69 +++++++++++----------
 1 file changed, 37 insertions(+), 32 deletions(-)

diff --git a/Sources/SWBGenericUnixPlatform/Plugin.swift b/Sources/SWBGenericUnixPlatform/Plugin.swift
index a3839068..0d6cd88c 100644
--- a/Sources/SWBGenericUnixPlatform/Plugin.swift
+++ b/Sources/SWBGenericUnixPlatform/Plugin.swift
@@ -129,39 +129,44 @@ struct GenericUnixToolchainRegistryExtension: ToolchainRegistryExtension {
 
         let fs = context.fs
 
-        if let swift = StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift")), fs.exists(swift) {
-            let realSwiftPath = try fs.realpath(swift).dirname.normalize()
-            let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
-            let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
-            let path: Path
-            switch (hasUsrBin, hasUsrLocalBin) {
-            case (true, false):
-                path = realSwiftPath.dirname.dirname
-            case (false, true):
-                path = realSwiftPath.dirname.dirname.dirname
-            case (false, false):
-                throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
-            case (true, true):
-                preconditionFailure()
+        for swift in [
+            Environment.current["SWIFT_EXEC"].map(Path.init),
+            StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift"))
+        ].compactMap(\.self) {
+            if fs.exists(swift) {
+                let realSwiftPath = try fs.realpath(swift).dirname.normalize()
+                let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
+                let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
+                let path: Path
+                switch (hasUsrBin, hasUsrLocalBin) {
+                case (true, false):
+                    path = realSwiftPath.dirname.dirname
+                case (false, true):
+                    path = realSwiftPath.dirname.dirname.dirname
+                case (false, false):
+                    throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
+                case (true, true):
+                    preconditionFailure()
+                }
+                let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
+                let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
+                return [
+                    Toolchain(
+                        identifier: ToolchainRegistry.defaultToolchainIdentifier,
+                        displayName: "Default",
+                        version: Version(),
+                        aliases: ["default"],
+                        path: path,
+                        frameworkPaths: [],
+                        libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
+                        defaultSettings: [:],
+                        overrideSettings: [:],
+                        defaultSettingsWhenPrimary: [:],
+                        executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
+                        testingLibraryPlatformNames: [],
+                        fs: fs)
+                ]
             }
-            let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
-            let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
-            return [
-                Toolchain(
-                    identifier: ToolchainRegistry.defaultToolchainIdentifier,
-                    displayName: "Default",
-                    version: Version(),
-                    aliases: ["default"],
-                    path: path,
-                    frameworkPaths: [],
-                    libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
-                    defaultSettings: [:],
-                    overrideSettings: [:],
-                    defaultSettingsWhenPrimary: [:],
-                    executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
-                    testingLibraryPlatformNames: [],
-                    fs: fs)
-            ]
         }
 
         return []

From 419c192a6cc8c04c8132c7dc89ded86cc535b515 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Mon, 4 Aug 2025 10:58:45 -0700
Subject: [PATCH 39/49] Handle the OS version in the triple for FreeBSD

We're going to keep the versioned triple, so it needs to be passed through after all.
---
 Sources/SWBAndroidPlatform/AndroidSDK.swift   |  24 ---
 Sources/SWBAndroidPlatform/Plugin.swift       |  13 +-
 Sources/SWBGenericUnixPlatform/Plugin.swift   | 138 ++++++++++++------
 Sources/SWBTestSupport/CoreTestSupport.swift  |   2 +-
 Sources/SWBUtil/CMakeLists.txt                |   1 +
 Sources/SWBUtil/LLVMTriple.swift              |  50 +++++++
 .../SWBWindowsPlatform/VSInstallation.swift   |   4 +-
 7 files changed, 153 insertions(+), 79 deletions(-)
 create mode 100644 Sources/SWBUtil/LLVMTriple.swift

diff --git a/Sources/SWBAndroidPlatform/AndroidSDK.swift b/Sources/SWBAndroidPlatform/AndroidSDK.swift
index 3b934838..9a2c00a0 100644
--- a/Sources/SWBAndroidPlatform/AndroidSDK.swift
+++ b/Sources/SWBAndroidPlatform/AndroidSDK.swift
@@ -134,30 +134,6 @@ public import Foundation
                 case bits64 = 64
             }
 
-            @_spi(Testing) public struct LLVMTriple: Codable, Equatable, Sendable {
-                public var arch: String
-                public var vendor: String
-                public var system: String
-                public var environment: String
-
-                var description: String {
-                    "\(arch)-\(vendor)-\(system)-\(environment)"
-                }
-
-                public init(from decoder: any Decoder) throws {
-                    let container = try decoder.singleValueContainer()
-                    let triple = try container.decode(String.self)
-                    if let match = try #/(?<arch>.+)-(?<vendor>.+)-(?<system>.+)-(?<environment>.+)/#.wholeMatch(in: triple) {
-                        self.arch = String(match.output.arch)
-                        self.vendor = String(match.output.vendor)
-                        self.system = String(match.output.system)
-                        self.environment = String(match.output.environment)
-                    } else {
-                        throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid triple string: \(triple)")
-                    }
-                }
-            }
-
             public let bitness: Bitness
             public let `default`: Bool
             public let deprecated: Bool
diff --git a/Sources/SWBAndroidPlatform/Plugin.swift b/Sources/SWBAndroidPlatform/Plugin.swift
index 3eb0ca1d..117e7c6e 100644
--- a/Sources/SWBAndroidPlatform/Plugin.swift
+++ b/Sources/SWBAndroidPlatform/Plugin.swift
@@ -131,11 +131,14 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
         let abis = androidNdk.abis
         let deploymentTargetRange = androidNdk.deploymentTargetRange
 
-        let allPossibleTriples = abis.values.flatMap { abi in
-            (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
+        let allPossibleTriples = try abis.values.flatMap { abi in
+            try (max(deploymentTargetRange.min, abi.min_os_version)...deploymentTargetRange.max).map { deploymentTarget in
                 var triple = abi.llvm_triple
                 triple.vendor = "unknown" // Android NDK uses "none", Swift SDKs use "unknown"
-                triple.environment += "\(deploymentTarget)"
+                guard let env = triple.environment else {
+                    throw StubError.error("Android triples must have an environment")
+                }
+                triple.environment = "\(env)\(deploymentTarget)"
                 return triple
             }
         }.map(\.description)
@@ -173,8 +176,8 @@ struct AndroidPlatformExtension: PlatformInfoExtension {
             "CustomProperties": .plDict([
                 // Unlike most platforms, the Android version goes on the environment field rather than the system field
                 // FIXME: Make this configurable in a better way so we don't need to push build settings at the SDK definition level
-                "LLVM_TARGET_TRIPLE_OS_VERSION": .plString("linux"),
-                "LLVM_TARGET_TRIPLE_SUFFIX": .plString("-android$(ANDROID_DEPLOYMENT_TARGET)"),
+                "LLVM_TARGET_TRIPLE_OS_VERSION": .plString("$(SWIFT_PLATFORM_TARGET_PREFIX)"),
+                "LLVM_TARGET_TRIPLE_SUFFIX": .plString("-android$($(DEPLOYMENT_TARGET_SETTING_NAME))"),
             ].merging(swiftSettings, uniquingKeysWith: { _, new in new })),
             "SupportedTargets": .plDict([
                 "android": .plDict([
diff --git a/Sources/SWBGenericUnixPlatform/Plugin.swift b/Sources/SWBGenericUnixPlatform/Plugin.swift
index 0d6cd88c..632a4cae 100644
--- a/Sources/SWBGenericUnixPlatform/Plugin.swift
+++ b/Sources/SWBGenericUnixPlatform/Plugin.swift
@@ -15,11 +15,44 @@ import SWBCore
 import Foundation
 
 @PluginExtensionSystemActor public func initializePlugin(_ manager: PluginManager) {
+    let plugin = GenericUnixPlugin()
     manager.register(GenericUnixDeveloperDirectoryExtension(), type: DeveloperDirectoryExtensionPoint.self)
     manager.register(GenericUnixPlatformSpecsExtension(), type: SpecificationsExtensionPoint.self)
     manager.register(GenericUnixPlatformInfoExtension(), type: PlatformInfoExtensionPoint.self)
-    manager.register(GenericUnixSDKRegistryExtension(), type: SDKRegistryExtensionPoint.self)
-    manager.register(GenericUnixToolchainRegistryExtension(), type: ToolchainRegistryExtensionPoint.self)
+    manager.register(GenericUnixSDKRegistryExtension(plugin: plugin), type: SDKRegistryExtensionPoint.self)
+    manager.register(GenericUnixToolchainRegistryExtension(plugin: plugin), type: ToolchainRegistryExtensionPoint.self)
+}
+
+final class GenericUnixPlugin: Sendable {
+    func swiftExecutablePath(fs: any FSProxy) -> Path? {
+        [
+            Environment.current["SWIFT_EXEC"].map(Path.init),
+            StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift"))
+        ].compactMap { $0 }.first(where: fs.exists)
+    }
+
+    func swiftTargetInfo(swiftExecutablePath: Path) async throws -> SwiftTargetInfo {
+        let args = ["-print-target-info"]
+        let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: swiftExecutablePath.str), arguments: args)
+        guard executionResult.exitStatus.isSuccess else {
+            throw RunProcessNonZeroExitError(args: [swiftExecutablePath.str] + args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
+        }
+        return try JSONDecoder().decode(SwiftTargetInfo.self, from: executionResult.stdout)
+    }
+}
+
+struct SwiftTargetInfo: Decodable {
+    struct TargetInfo: Decodable {
+        let triple: LLVMTriple
+        let unversionedTriple: LLVMTriple
+    }
+    let target: TargetInfo
+}
+
+extension SwiftTargetInfo.TargetInfo {
+    var tripleVersion: String? {
+        triple != unversionedTriple && triple.system.hasPrefix(unversionedTriple.system) ? String(triple.system.dropFirst(unversionedTriple.system.count)).nilIfEmpty : nil
+    }
 }
 
 struct GenericUnixDeveloperDirectoryExtension: DeveloperDirectoryExtension {
@@ -69,9 +102,11 @@ struct GenericUnixPlatformInfoExtension: PlatformInfoExtension {
 }
 
 struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
+    let plugin: GenericUnixPlugin
+
     func additionalSDKs(context: any SDKRegistryExtensionAdditionalSDKsContext) async throws -> [(path: Path, platform: SWBCore.Platform?, data: [String: PropertyListItem])] {
         let operatingSystem = context.hostOperatingSystem
-        guard operatingSystem.createFallbackSystemToolchain, let platform = try context.platformRegistry.lookup(name: operatingSystem.xcodePlatformName) else {
+        guard operatingSystem.createFallbackSystemToolchain, let platform = try context.platformRegistry.lookup(name: operatingSystem.xcodePlatformName), let swift = plugin.swiftExecutablePath(fs: context.fs) else {
             return []
         }
 
@@ -100,6 +135,23 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
             tripleEnvironment = ""
         }
 
+        let swiftTargetInfo = try await plugin.swiftTargetInfo(swiftExecutablePath: swift)
+
+        let deploymentTargetSettings: [String: PropertyListItem]
+        if operatingSystem == .freebsd {
+            guard let tripleVersion = swiftTargetInfo.target.tripleVersion else {
+                throw StubError.error("Unknown FreeBSD triple version")
+            }
+            deploymentTargetSettings = [
+                "DeploymentTargetSettingName": .plString("FREEBSD_DEPLOYMENT_TARGET"),
+                "DefaultDeploymentTarget": .plString(tripleVersion),
+                "MinimumDeploymentTarget": .plString(tripleVersion),
+                "MaximumDeploymentTarget": .plString(tripleVersion),
+            ]
+        } else {
+            deploymentTargetSettings = [:]
+        }
+
         return try [(.root, platform, [
             "Type": .plString("SDK"),
             "Version": .plString(Version(ProcessInfo.processInfo.operatingSystemVersion).zeroTrimmed.description),
@@ -114,62 +166,54 @@ struct GenericUnixSDKRegistryExtension: SDKRegistryExtension {
                     "LLVMTargetTripleEnvironment": .plString(tripleEnvironment),
                     "LLVMTargetTripleSys": .plString(operatingSystem.xcodePlatformName),
                     "LLVMTargetTripleVendor": .plString("unknown"),
-                ])
+                ].merging(deploymentTargetSettings, uniquingKeysWith: { _, new in new }))
             ]),
         ])]
     }
 }
 
 struct GenericUnixToolchainRegistryExtension: ToolchainRegistryExtension {
+    let plugin: GenericUnixPlugin
+
     func additionalToolchains(context: any ToolchainRegistryExtensionAdditionalToolchainsContext) async throws -> [Toolchain] {
         let operatingSystem = context.hostOperatingSystem
-        guard operatingSystem.createFallbackSystemToolchain else {
+        let fs = context.fs
+        guard operatingSystem.createFallbackSystemToolchain, let swift = plugin.swiftExecutablePath(fs: fs) else {
             return []
         }
 
-        let fs = context.fs
-
-        for swift in [
-            Environment.current["SWIFT_EXEC"].map(Path.init),
-            StackedSearchPath(environment: .current, fs: fs).lookup(Path("swift"))
-        ].compactMap(\.self) {
-            if fs.exists(swift) {
-                let realSwiftPath = try fs.realpath(swift).dirname.normalize()
-                let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
-                let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
-                let path: Path
-                switch (hasUsrBin, hasUsrLocalBin) {
-                case (true, false):
-                    path = realSwiftPath.dirname.dirname
-                case (false, true):
-                    path = realSwiftPath.dirname.dirname.dirname
-                case (false, false):
-                    throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
-                case (true, true):
-                    preconditionFailure()
-                }
-                let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
-                let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
-                return [
-                    Toolchain(
-                        identifier: ToolchainRegistry.defaultToolchainIdentifier,
-                        displayName: "Default",
-                        version: Version(),
-                        aliases: ["default"],
-                        path: path,
-                        frameworkPaths: [],
-                        libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
-                        defaultSettings: [:],
-                        overrideSettings: [:],
-                        defaultSettingsWhenPrimary: [:],
-                        executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
-                        testingLibraryPlatformNames: [],
-                        fs: fs)
-                ]
-            }
+        let realSwiftPath = try fs.realpath(swift).dirname.normalize()
+        let hasUsrBin = realSwiftPath.str.hasSuffix("/usr/bin")
+        let hasUsrLocalBin = realSwiftPath.str.hasSuffix("/usr/local/bin")
+        let path: Path
+        switch (hasUsrBin, hasUsrLocalBin) {
+        case (true, false):
+            path = realSwiftPath.dirname.dirname
+        case (false, true):
+            path = realSwiftPath.dirname.dirname.dirname
+        case (false, false):
+            throw StubError.error("Unexpected toolchain layout for Swift installation path: \(realSwiftPath)")
+        case (true, true):
+            preconditionFailure()
         }
-
-        return []
+        let llvmDirectories = try Array(fs.listdir(Path("/usr/lib")).filter { $0.hasPrefix("llvm-") }.sorted().reversed())
+        let llvmDirectoriesLocal = try Array(fs.listdir(Path("/usr/local")).filter { $0.hasPrefix("llvm") }.sorted().reversed())
+        return [
+            Toolchain(
+                identifier: ToolchainRegistry.defaultToolchainIdentifier,
+                displayName: "Default",
+                version: Version(),
+                aliases: ["default"],
+                path: path,
+                frameworkPaths: [],
+                libraryPaths: llvmDirectories.map { "/usr/lib/\($0)/lib" } + llvmDirectoriesLocal.map { "/usr/local/\($0)/lib" } + ["/usr/lib64"],
+                defaultSettings: [:],
+                overrideSettings: [:],
+                defaultSettingsWhenPrimary: [:],
+                executableSearchPaths: realSwiftPath.dirname.relativeSubpath(from: path).map { [path.join($0).join("bin")] } ?? [],
+                testingLibraryPlatformNames: [],
+                fs: fs)
+        ]
     }
 }
 
diff --git a/Sources/SWBTestSupport/CoreTestSupport.swift b/Sources/SWBTestSupport/CoreTestSupport.swift
index 1bf280ea..5ce4bedf 100644
--- a/Sources/SWBTestSupport/CoreTestSupport.swift
+++ b/Sources/SWBTestSupport/CoreTestSupport.swift
@@ -56,7 +56,7 @@ extension Core {
             developerPath = .xcode(xcodeDeveloperDirPath)
         } else {
             // In the context of auto-generated package schemes, try to infer the active Xcode.
-            let potentialDeveloperPath = getEnvironmentVariable("PATH")?.components(separatedBy: String(Path.pathEnvironmentSeparator)).first.map(Path.init)?.dirname.dirname
+            let potentialDeveloperPath = getEnvironmentVariable(.path)?.components(separatedBy: String(Path.pathEnvironmentSeparator)).first.map(Path.init)?.dirname.dirname
             let versionInfo = potentialDeveloperPath?.dirname.join("version.plist")
             if let versionInfo = versionInfo, (try? PropertyList.fromPath(versionInfo, fs: localFS))?.dictValue?["ProjectName"] == "IDEApplication" {
                 developerPath = potentialDeveloperPath.map { .xcode($0) }
diff --git a/Sources/SWBUtil/CMakeLists.txt b/Sources/SWBUtil/CMakeLists.txt
index 91348fd9..b366c370 100644
--- a/Sources/SWBUtil/CMakeLists.txt
+++ b/Sources/SWBUtil/CMakeLists.txt
@@ -54,6 +54,7 @@ add_library(SWBUtil
   LazyCache.swift
   Library.swift
   LineReader.swift
+  LLVMTriple.swift
   Lock.swift
   MachO.swift
   Math.swift
diff --git a/Sources/SWBUtil/LLVMTriple.swift b/Sources/SWBUtil/LLVMTriple.swift
new file mode 100644
index 00000000..2ec27fa5
--- /dev/null
+++ b/Sources/SWBUtil/LLVMTriple.swift
@@ -0,0 +1,50 @@
+//===----------------------------------------------------------------------===//
+//
+// This source file is part of the Swift open source project
+//
+// Copyright (c) 2025 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+//===----------------------------------------------------------------------===//
+
+public struct LLVMTriple: Decodable, Equatable, Sendable, CustomStringConvertible {
+    public var arch: String
+    public var vendor: String
+    public var system: String
+    public var environment: String?
+
+    public var description: String {
+        if let environment {
+            return "\(arch)-\(vendor)-\(system)-\(environment)"
+        }
+        return "\(arch)-\(vendor)-\(system)"
+    }
+
+    public init(_ string: String) throws {
+        guard let match = try #/(?<arch>[^-]+)-(?<vendor>[^-]+)-(?<system>[^-]+)(-(?<environment>[^-]+))?/#.wholeMatch(in: string) else {
+            throw LLVMTripleError.invalidTripleStringFormat(string)
+        }
+        self.arch = String(match.output.arch)
+        self.vendor = String(match.output.vendor)
+        self.system = String(match.output.system)
+        self.environment = match.output.environment.map { String($0) }
+    }
+
+    public init(from decoder: any Swift.Decoder) throws {
+        self = try Self(decoder.singleValueContainer().decode(String.self))
+    }
+}
+
+enum LLVMTripleError: Error, CustomStringConvertible {
+    case invalidTripleStringFormat(String)
+
+    var description: String {
+        switch self {
+        case let .invalidTripleStringFormat(tripleString):
+            "Invalid triple string format: \(tripleString)"
+        }
+    }
+}
diff --git a/Sources/SWBWindowsPlatform/VSInstallation.swift b/Sources/SWBWindowsPlatform/VSInstallation.swift
index 23ab705c..68c35abb 100644
--- a/Sources/SWBWindowsPlatform/VSInstallation.swift
+++ b/Sources/SWBWindowsPlatform/VSInstallation.swift
@@ -57,14 +57,14 @@ public struct VSInstallation: Decodable, Sendable {
         ]
         let executionResult = try await Process.getOutput(url: URL(fileURLWithPath: vswhere.str), arguments: args)
         guard executionResult.exitStatus.isSuccess else {
-            throw RunProcessNonZeroExitError(args: args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
+            throw RunProcessNonZeroExitError(args: [vswhere.str] + args, workingDirectory: nil, environment: [:], status: executionResult.exitStatus, stdout: ByteString(executionResult.stdout), stderr: ByteString(executionResult.stderr))
         }
         return try JSONDecoder().decode([VSInstallation].self, from: executionResult.stdout)
     }
 
     private static func vswherePath(fs: any FSProxy) throws -> Path? {
         var paths: [Path] = []
-        if let path = try POSIX.getenv("PATH") {
+        if let path = getEnvironmentVariable(.path) {
             paths.append(contentsOf: path.split(separator: Path.pathEnvironmentSeparator).map(Path.init).filter {
                 // PATH may contain unexpanded shell variable references
                 $0.isAbsolute

From 7bdc27536385d3e2a750711451b09fade081f81c Mon Sep 17 00:00:00 2001
From: Matt Seaman <mseaman@apple.com>
Date: Wed, 6 Aug 2025 12:23:08 -0700
Subject: [PATCH 40/49] Generalize relocating source-generating resource files
 to the sources phase (#705)

---
 .../FilesBasedBuildPhaseTaskProducer.swift    | 164 ++++++++++++------
 .../ResourcesTaskProducer.swift               |  20 ++-
 .../SourcesTaskProducer.swift                 |  51 +-----
 3 files changed, 139 insertions(+), 96 deletions(-)

diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
index d6cb900b..0a76ccfb 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/FilesBasedBuildPhaseTaskProducer.swift
@@ -84,7 +84,12 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
     let belongsToPreferredArch: Bool
     let currentArchSpec: ArchitectureSpec?
 
-    package init(_ scope: MacroEvaluationScope, belongsToPreferredArch: Bool = true, currentArchSpec: ArchitectureSpec? = nil, resolveBuildRules: Bool = true, resourcesDir: Path? = nil, tmpResourcesDir: Path? = nil) {
+    /// If `true`, avoid emitting any diagnostics via the task producer context.
+    ///
+    /// This might be set in cases where `BuildFilesProcessingContext` is being used for ephemeral grouping operations outside of the main grouping routine.
+    private let repressDiagnostics: Bool
+
+    package init(_ scope: MacroEvaluationScope, belongsToPreferredArch: Bool = true, currentArchSpec: ArchitectureSpec? = nil, resolveBuildRules: Bool = true, resourcesDir: Path? = nil, tmpResourcesDir: Path? = nil, repressDiagnostics: Bool = false) {
         // Define the predicates for filtering source files.
         //
         // FIXME: Factor this out, and make this machinery efficient.
@@ -97,6 +102,7 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
         self.belongsToPreferredArch = belongsToPreferredArch
         self.currentArchSpec = currentArchSpec
         self.currentPlatformFilter = PlatformFilter(scope)
+        self.repressDiagnostics = repressDiagnostics
     }
 
     /// Adds the file to build to the appropriate group for the task producer being processed, including resolving a build rule action for that group if appropriate.
@@ -113,8 +119,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
         let buildRuleMatchResult = taskProducerContext.buildRuleSet.match(ftb, scope)
         let provisionalRuleAction = buildRuleMatchResult.action
 
-        for diagnostic in buildRuleMatchResult.diagnostics {
-            taskProducerContext.emit(diagnostic.behavior, diagnostic.message)
+        if !repressDiagnostics {
+            for diagnostic in buildRuleMatchResult.diagnostics {
+                taskProducerContext.emit(diagnostic.behavior, diagnostic.message)
+            }
         }
 
         // If this file is the output of some task, then we perform some checks to see whether we should process it.
@@ -125,8 +133,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
             if generatedByBuildRuleAction === provisionalRuleAction {
                 // If we should not add if we didn't find an appropriate build rule, then emit a warning and return.
                 guard addIfNoBuildRuleFound else {
-                    let currentArch = scope.evaluate(BuiltinMacros.CURRENT_ARCH)
-                    taskProducerContext.warning("no rule to process file '\(ftb.absolutePath.str)' of type '\(ftb.fileType.identifier)'" + (currentArch != "undefined_arch" ? " for architecture '\(scope.evaluate(BuiltinMacros.CURRENT_ARCH))'" : ""))
+                    if !repressDiagnostics {
+                        let currentArch = scope.evaluate(BuiltinMacros.CURRENT_ARCH)
+                        taskProducerContext.warning("no rule to process file '\(ftb.absolutePath.str)' of type '\(ftb.fileType.identifier)'" + (currentArch != "undefined_arch" ? " for architecture '\(scope.evaluate(BuiltinMacros.CURRENT_ARCH))'" : ""))
+                    }
                     return
                 }
                 // If we should always add, then do so as an ungrouped file.
@@ -183,7 +193,10 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
 
         // If we've already processed a group with this identifier, then emit an error to the user as this is likely a project configuration error. For example, if a project is generating code (e.g. from a build rule) and multiple versions of the same file are being generated, and thus being processed, this is potentially very bad, especially if those files don't contain the same output!
         guard !processedGroupIdents.contains(groupIdent) else {
-            return taskProducerContext.error("the file group with identifier '\(groupIdent)' has already been processed.")
+            if !repressDiagnostics {
+                taskProducerContext.error("the file group with identifier '\(groupIdent)' has already been processed.")
+            }
+            return
         }
 
         // Find or create the group for the identifier we got back.
@@ -214,30 +227,43 @@ package final class BuildFilesProcessingContext: BuildFileFilteringContext {
 
     /// Allow `collectionGroups` to subsume `singletonGroups`. The initial pass in groupAndAddTasksForFiles looks at one file at a time to assign rules and groups. Certain grouping strategies need to inspect multiple files to group them (e.g. sticker packs need to group an asset catalog and loose strings files matching the sticker pack - without grouping every other strings file as well). This function allows each collectionGroup to inspect
     fileprivate func mergeGroups(_ context: TaskProducerContext) {
+        let allGroupedSingletonGroups = subsumeAdditionalFilesIfDesired(from: self.singletonGroups, context)
+
+        if !allGroupedSingletonGroups.isEmpty {
+            self.singletonGroups = Queue(self.singletonGroups.filter { !allGroupedSingletonGroups.contains($0) })
+        }
+    }
+
+    /// Allow `collectionGroups` to subsume `filesToSubsume` if desired.
+    ///
+    /// There is no guarantee that all or even any of the files in `filesToSubsume` will actually be subsumed.
+    ///
+    /// This method will never add additional groups. It can only add to existing ones.
+    ///
+    /// - returns: The files that were subsumed.
+    fileprivate func subsumeAdditionalFilesIfDesired(from filesToSubsume: some Sequence<FileToBuildGroup>, _ context: TaskProducerContext) -> Set<FileToBuildGroup> {
         // This assumes that groupAdditionalFiles() rarely chooses to group anything.
 
-        var allGroupedSingletonGroups = Set<FileToBuildGroup>()
+        var allSubsumedGroups = Set<FileToBuildGroup>()
         for collectionGroup in collectionGroups {
             if let rule = collectionGroup.assignedBuildRuleAction {
                 for grouper in rule.inputFileGroupingStrategies {
-                    let groupedSingletonGroups = grouper.groupAdditionalFiles(to: collectionGroup, from: self.singletonGroups, context: context)
+                    let subsumedGroups = grouper.groupAdditionalFiles(to: collectionGroup, from: filesToSubsume, context: context)
 
-                    for group in groupedSingletonGroups {
+                    for group in subsumedGroups {
                         collectionGroup.files.append(contentsOf: group.files)
 
-                        if allGroupedSingletonGroups.contains(group) {
+                        if !repressDiagnostics && allSubsumedGroups.contains(group) {
                             context.error("Multiple rules merged: \(group.files[0].absolutePath)")
                         }
                     }
 
-                    allGroupedSingletonGroups.formUnion(groupedSingletonGroups)
+                    allSubsumedGroups.formUnion(subsumedGroups)
                 }
             }
         }
 
-        if !allGroupedSingletonGroups.isEmpty {
-            self.singletonGroups = Queue(self.singletonGroups.filter { !allGroupedSingletonGroups.contains($0) })
-        }
+        return allSubsumedGroups
     }
 
     // Returns the next file group to process, or nil if all groups have been processed.
@@ -295,6 +321,19 @@ extension TaskProducerContext {
     }
 }
 
+extension PluginManager {
+    /// Returns identifiers of file types that can generate sources, and therefore need to be processed within the Sources build phase (at least if there are any existing source files).
+    ///
+    /// Asset Catalogs would be one example of this, so that they can generate symbols.
+    func fileTypesProducingGeneratedSources() -> [String] {
+        var compileToSwiftFileTypes : [String] = []
+        for groupingStragegyExtensions in extensions(of: InputFileGroupingStrategyExtensionPoint.self) {
+            compileToSwiftFileTypes.append(contentsOf: groupingStragegyExtensions.fileTypesCompilingToSwiftSources())
+        }
+        return compileToSwiftFileTypes
+    }
+}
+
 // MARK:
 
 
@@ -352,7 +391,12 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
     }
 
     /// Allows subclasses to contribute additional build files.
-    func additionalBuildFiles(_ scope: MacroEvaluationScope) -> [SWBCore.BuildFile] {
+    func additionalBuildFiles(_ scope: MacroEvaluationScope) async -> [SWBCore.BuildFile] {
+        return []
+    }
+
+    /// Allows subclasses to specify build files that should be skipped by this task producer.
+    func buildFilesToSkip(_ scope: MacroEvaluationScope) async -> Set<Ref<SWBCore.BuildFile>> {
         return []
     }
 
@@ -376,14 +420,6 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
 
         let buildPhaseFileWarningContext = BuildPhaseFileWarningContext(context, scope)
 
-        // Sadly we need to make various decisions based on codegen of Asset and String Catalogs.
-        // We can remove this when we get rid of build phases.
-        let sourceFileCount = (self.targetContext.configuredTarget?.target as? SWBCore.StandardTarget)?.sourcesBuildPhase?.buildFiles.count ?? 0
-        let stringsFileTypes = ["text.plist.strings", "text.plist.stringsdict"].map { context.lookupFileType(identifier: $0)! }
-        var xcstringsBases = Set<String>()
-        let shouldCodeGenAssets = scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) && sourceFileCount > 0
-        let shouldCodeGenStrings = scope.evaluate(BuiltinMacros.STRING_CATALOG_GENERATE_SYMBOLS) && sourceFileCount > 0
-
         // Helper function for adding a resolved item.  The build file can be nil here if the client wants to add a file divorced from any build file (e.g., because the build file contains context which shouldn't be applied to this file).
         func addResolvedItem(buildFile: SWBCore.BuildFile?, path: Path, reference: SWBCore.Reference?, fileType: FileTypeSpec, shouldUsePrefixHeader: Bool = true) {
             let base = path.basenameWithoutSuffix.lowercased()
@@ -408,26 +444,16 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
             addResolvedItem(buildFile: nil, path: path, reference: nil, fileType: fileType, shouldUsePrefixHeader: shouldUsePrefixHeader)
         }
 
-        for buildFile in buildPhase.buildFiles + additionalBuildFiles(scope) {
+        let buildFilesToSkip = await self.buildFilesToSkip(scope)
+        for buildFile in await buildPhase.buildFiles + additionalBuildFiles(scope) {
+            guard !buildFilesToSkip.contains(Ref(buildFile)) else {
+                continue
+            }
+
             // Resolve the reference.
             do {
                 let (reference, path, fileType) = try context.resolveBuildFileReference(buildFile)
 
-                if shouldCodeGenAssets {
-                    // Ignore xcassets in Resource Copy Phase since they're now added to the Compile Sources phase for codegen.
-                    if producer.buildPhase is SWBCore.ResourcesBuildPhase && fileType.conformsTo(identifier: "folder.abstractassetcatalog") {
-                        continue
-                    }
-                }
-                if shouldCodeGenStrings {
-                    // Ignore xcstrings in Resource Copy Phase since they're now added to the Compile Sources phase for codegen.
-                    if producer.buildPhase is SWBCore.ResourcesBuildPhase && fileType.conformsTo(identifier: "text.json.xcstrings") {
-                        // Keep the basename because later we need to ignore same-named .strings/dict files as well.
-                        xcstringsBases.insert(path.basenameWithoutSuffix)
-                        continue
-                    }
-                }
-
                 // Compilation of .rkassets depends on additional auxiliary inputs that are not
                 // accessible from a spec class. Instead, they are handled entirely by their own
                 // task producer (RealityAssetsTaskProducer), so skip processing them as part of
@@ -585,14 +611,10 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
             }
         }
 
-        var compileToSwiftFileTypes : [String] = []
-        for groupingStragegyExtensions in await context.workspaceContext.core.pluginManager.extensions(of: InputFileGroupingStrategyExtensionPoint.self) {
-            compileToSwiftFileTypes.append(contentsOf: groupingStragegyExtensions.fileTypesCompilingToSwiftSources())
-        }
-
         // Reorder resolvedBuildFiles so that file types which compile to Swift appear first in the list and so are processed first.
         // This is needed because generated sources aren't added to the the main source code list.
         // rdar://102834701 (File grouping for 'collection groups' is sensitive to ordering of build phase members)
+        let compileToSwiftFileTypes = await context.workspaceContext.core.pluginManager.fileTypesProducingGeneratedSources()
         var compileToSwiftFiles = [ResolvedBuildFile]()
         var otherBuildFiles = [ResolvedBuildFile]()
         for resolvedBuildFile in resolvedBuildFiles {
@@ -645,14 +667,6 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
                 continue
             }
 
-            // Ignore certain .strings/dict files in Resources phase when codegen for xcstrings is enabled.
-            if shouldCodeGenStrings &&
-                producer.buildPhase is SWBCore.ResourcesBuildPhase &&
-                fileType.conformsToAny(stringsFileTypes) &&
-                xcstringsBases.contains(path.basenameWithoutSuffix) {
-                continue
-            }
-
             // Have the build files context add the file to the appropriate file group.
             buildFilesContext.addFile(fileToBuild, context, scope)
         }
@@ -770,6 +784,52 @@ package class FilesBasedBuildPhaseTaskProducerBase: PhasedTaskProducer {
         }
     }
 
+    /// Filters `buildFiles` down to only those files that are necessary inputs to source code generation.
+    ///
+    /// For example, this could include Asset Catalogs (and any of the files they subsume in their grouping strategy).
+    func sourceGenerationInputFiles(from buildFiles: [SWBCore.BuildFile], scope: MacroEvaluationScope) async -> [SWBCore.BuildFile] {
+        guard !buildFiles.isEmpty else {
+            return []
+        }
+
+        let fileIdentifiersGeneratingSources = await context.workspaceContext.core.pluginManager.fileTypesProducingGeneratedSources()
+        guard !fileIdentifiersGeneratingSources.isEmpty else {
+            return []
+        }
+
+        var grouper: BuildFilesProcessingContext?
+        var ungroupedFiles = [FileToBuild]()
+        for buildFile in buildFiles {
+            guard let fileRef = try? targetContext.resolveBuildFileReference(buildFile) else {
+                continue
+            }
+
+            let ftb = FileToBuild(absolutePath: fileRef.absolutePath, fileType: fileRef.fileType, buildFile: buildFile, regionVariantName: fileRef.absolutePath.regionVariantName)
+
+            guard fileIdentifiersGeneratingSources.contains(where: { identifier in fileRef.fileType.conformsTo(identifier: identifier) }) else {
+                ungroupedFiles.append(ftb)
+                continue
+            }
+
+            if grouper == nil {
+                grouper = BuildFilesProcessingContext(scope, repressDiagnostics: true)
+            }
+
+            grouper?.addFile(ftb, context, scope)
+        }
+
+        guard let grouper else {
+            return []
+        }
+
+        let remainingFiles = ungroupedFiles.map { ftb in
+            FileToBuildGroup(ftb.absolutePath.str, files: [ftb], action: nil)
+        }
+        _ = grouper.subsumeAdditionalFilesIfDesired(from: remainingFiles, context)
+
+        return grouper.collectionGroups.flatMap(\.files).compactMap(\.buildFile)
+    }
+
     /// This method is used by the `installLoc` build action to return the paths to localized content within a bundle.
     /// For example a bundle which is part of the project sources, where only the localized content in the bundle should be copied in the `installLoc` action.
     /// - returns: A list of path strings relative to the absolute path of `ftb`.
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift
index 222a0ea4..848d3d57 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/ResourcesTaskProducer.swift
@@ -195,12 +195,28 @@ final class ResourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, FilesBa
         }
     }
 
+    override func buildFilesToSkip(_ scope: MacroEvaluationScope) async -> Set<Ref<SWBCore.BuildFile>> {
+        // Some files might generate sources (e.g. generating symbols) and thus were moved to the Sources phase.
+        // So they should be skipped in Resources.
+
+        let standardTarget = targetContext.configuredTarget?.target as? StandardTarget
+        let sourceFiles = standardTarget?.sourcesBuildPhase?.buildFiles ?? []
+        let resourceFiles = standardTarget?.resourcesBuildPhase?.buildFiles ?? []
+
+        guard !sourceFiles.isEmpty && !resourceFiles.isEmpty else {
+            return []
+        }
+
+        let files = await sourceGenerationInputFiles(from: resourceFiles, scope: scope)
+        return Set(files.map({ Ref($0) }))
+    }
+
     override func additionalFilesToBuild(_ scope: MacroEvaluationScope) -> [FileToBuild] {
         var additionalFilesToBuild: [FileToBuild] = []
 
-        // Add the generated xcassets when we're not generating asset symbols since we'll be handling the other xcassets here as well.
+        // Add the generated xcassets when there are no sources since we'll be handling the other xcassets here as well.
         let sourceFiles = (self.targetContext.configuredTarget?.target as? StandardTarget)?.sourcesBuildPhase?.buildFiles.count ?? 0
-        if (!scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) || sourceFiles == 0) && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
+        if sourceFiles == 0 && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
             let assetCatalogToBeGenerated = scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATED_ASSET_CATALOG_FILE)
             additionalFilesToBuild.append(
                 FileToBuild(absolutePath: assetCatalogToBeGenerated, inferringTypeUsing: context)
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
index 154a25e7..8a4ff447 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
@@ -249,58 +249,25 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
         super.init(context, buildPhase: sourcesBuildPhase, phaseStartNodes: phaseStartNodes, phaseEndNode: phaseEndNode, phaseEndTask: phaseEndTask)
     }
 
-    override func additionalBuildFiles(_ scope: MacroEvaluationScope) -> [BuildFile] {
-        var additionalBuildFiles = [BuildFile]()
-
-        // Both Asset Catalogs and String Catalogs need moved to the Sources phase to enable codegen.
-        // This isn't great but can be removed once we eliminate build phases.
+    override func additionalBuildFiles(_ scope: MacroEvaluationScope) async -> [BuildFile] {
+        // Some files might generate sources (e.g. generating symbols) and thus need to be in the Sources phase.
 
         let standardTarget = targetContext.configuredTarget?.target as? StandardTarget
-        let sourceFiles = standardTarget?.sourcesBuildPhase?.buildFiles.count ?? 0
-
-        if scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) && (sourceFiles > 0) {
-            // Add xcassets to Compile Sources phase to enable codegen.
-            let catalogs = standardTarget?.resourcesBuildPhase?.buildFiles.filter { buildFile in
-                isAssetCatalog(scope: scope, buildFile: buildFile, context: targetContext, includeGenerated: true)
-            } ?? []
-            additionalBuildFiles.append(contentsOf: catalogs)
-        }
-
-        if scope.evaluate(BuiltinMacros.STRING_CATALOG_GENERATE_SYMBOLS) && (sourceFiles > 0) {
-            let allResources = standardTarget?.resourcesBuildPhase?.buildFiles ?? []
-            var stringCatalogs = [BuildFile]()
-            var stringTableNames = Set<String>()
-            var extraFiles = [BuildFile]()
-
-            // Add xcstrings to Compile Sources phase to enable codegen.
-            for buildFile in allResources {
-                if let fileRef = try? targetContext.resolveBuildFileReference(buildFile), fileRef.fileType.conformsTo(identifier: "text.json.xcstrings") {
-                    stringTableNames.insert(fileRef.absolutePath.basenameWithoutSuffix)
-                    stringCatalogs.append(buildFile)
-                }
-            }
+        let sourceFiles = standardTarget?.sourcesBuildPhase?.buildFiles ?? []
+        let resourceFiles = standardTarget?.resourcesBuildPhase?.buildFiles ?? []
 
-            // The xcstrings file grouping strategy also subsumes same-named .strings and .stringsdict files.
-            let stringsFileTypes = ["text.plist.strings", "text.plist.stringsdict"].map { context.lookupFileType(identifier: $0)! }
-            for buildFile in allResources {
-                if let fileRef = try? targetContext.resolveBuildFileReference(buildFile),
-                   fileRef.fileType.conformsToAny(stringsFileTypes),
-                   stringTableNames.contains(fileRef.absolutePath.basenameWithoutSuffix) {
-                    extraFiles.append(buildFile)
-                }
-            }
-
-            additionalBuildFiles.append(contentsOf: stringCatalogs + extraFiles)
+        guard !sourceFiles.isEmpty && !resourceFiles.isEmpty else {
+            return []
         }
 
-        return additionalBuildFiles
+        return await sourceGenerationInputFiles(from: resourceFiles, scope: scope)
     }
 
     override func additionalFilesToBuild(_ scope: MacroEvaluationScope) -> [FileToBuild] {
         var additionalFilesToBuild: [FileToBuild] = []
         let sourceFiles = (self.targetContext.configuredTarget?.target as? StandardTarget)?.sourcesBuildPhase?.buildFiles.count ?? 0
-        if scope.evaluate(BuiltinMacros.ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS) && sourceFiles > 0 && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
-            // Add the generated xcassets if we're generating asset symbols since we'll be handling the other xcassets here as well.
+        if sourceFiles > 0 && scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATE_ASSET_CATALOG) {
+            // Add the generated xcassets since we'll be handling the other xcassets here as well.
             let assetCatalogToBeGenerated = scope.evaluate(BuiltinMacros.APP_PLAYGROUND_GENERATED_ASSET_CATALOG_FILE)
             additionalFilesToBuild.append(
                 FileToBuild(absolutePath: assetCatalogToBeGenerated, inferringTypeUsing: context)

From 4a84ad562fd589b7cbf264bd49e6df40e2c11ef1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Boris=20B=C3=BCgling?= <bbuegling@apple.com>
Date: Wed, 6 Aug 2025 16:21:56 -0700
Subject: [PATCH 41/49] Fix task signatures for module dependencies (#710)

- `ValidateDependencies` needs to include the context as part of its signature
- Swift tasks no longer need to do this since they don't deal in module dependencies directly, only emitting a data file for use by `ValidateDependencies`
---
 .../Tools/SwiftCompiler.swift                 | 19 +------------------
 .../Tools/ValidateDependencies.swift          | 12 +++++++++++-
 2 files changed, 12 insertions(+), 19 deletions(-)

diff --git a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
index 09c35dba..44b99a76 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/SwiftCompiler.swift
@@ -2405,25 +2405,8 @@ public final class SwiftCompilerSpec : CompilerSpec, SpecIdentifierType, SwiftDi
                     dependencyData = nil
                 }
 
-                let compilationRequirementAdditionalSignatureData: String
-                if let moduleDependenciesContext = cbc.producer.moduleDependenciesContext {
-                    do {
-                        let jsonData = try JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(moduleDependenciesContext)
-                        guard let signature = String(data: jsonData, encoding: .utf8) else {
-                            throw StubError.error("non-UTF-8 data")
-                        }
-                        compilationRequirementAdditionalSignatureData = additionalSignatureData + "|\(signature)"
-                    } catch {
-                        delegate.error("failed to serialize 'MODULE_DEPENDENCIES' context information: \(error)")
-                        return
-                    }
-                }
-                else {
-                    compilationRequirementAdditionalSignatureData = additionalSignatureData
-                }
-
                 // Compilation Requirements
-                delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo("SwiftDriver Compilation Requirements", targetName), additionalSignatureData: compilationRequirementAdditionalSignatureData, commandLine: ["builtin-Swift-Compilation-Requirements", "--"] + args, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: allInputsNodes, outputs: compilationRequirementOutputs, action: delegate.taskActionCreationDelegate.createSwiftCompilationRequirementTaskAction(), execDescription: archSpecificExecutionDescription(cbc.scope.namespace.parseString("Unblock downstream dependents of $PRODUCT_NAME"), cbc, delegate), preparesForIndexing: true, enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilation, .compilationRequirement, .linkingRequirement, .blockedByTargetHeaders, .compilationForIndexableSourceFile], usesExecutionInputs: true, showInLog: true)
+                delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo("SwiftDriver Compilation Requirements", targetName), additionalSignatureData: additionalSignatureData, commandLine: ["builtin-Swift-Compilation-Requirements", "--"] + args, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: allInputsNodes, outputs: compilationRequirementOutputs, action: delegate.taskActionCreationDelegate.createSwiftCompilationRequirementTaskAction(), execDescription: archSpecificExecutionDescription(cbc.scope.namespace.parseString("Unblock downstream dependents of $PRODUCT_NAME"), cbc, delegate), preparesForIndexing: true, enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilation, .compilationRequirement, .linkingRequirement, .blockedByTargetHeaders, .compilationForIndexableSourceFile], usesExecutionInputs: true, showInLog: true)
 
                 if case .compile = compilationMode {
                     // Unblocking compilation
diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
index 0fb14969..43021306 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
@@ -10,6 +10,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+private import Foundation
 public import SWBUtil
 import SWBMacro
 
@@ -39,8 +40,17 @@ public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementa
         guard let configuredTarget = cbc.producer.configuredTarget else {
             return
         }
+
+        let jsonData: Data
+        do {
+            jsonData = try JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(payload)
+        } catch {
+            delegate.error("Error serializing \(payload): \(error)")
+            return
+        }
+        let signature = String(decoding: jsonData, as: UTF8.self)
         let output =  delegate.createVirtualNode("ValidateDependencies \(configuredTarget.guid)")
-        delegate.createTask(type: self, payload: payload, ruleInfo: ["ValidateDependencies"], commandLine: ["builtin-validate-dependencies"] + dependencyInfos.map { $0.path.str }, environment: EnvironmentBindings(), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: dependencyInfos + cbc.commandOrderingInputs, outputs: [output], action: delegate.taskActionCreationDelegate.createValidateDependenciesTaskAction(), preparesForIndexing: false, enableSandboxing: false)
+        delegate.createTask(type: self, payload: payload, ruleInfo: ["ValidateDependencies"], additionalSignatureData: signature, commandLine: ["builtin-validate-dependencies"] + dependencyInfos.map { $0.path.str }, environment: EnvironmentBindings(), workingDirectory: cbc.producer.defaultWorkingDirectory, inputs: dependencyInfos + cbc.commandOrderingInputs, outputs: [output], action: delegate.taskActionCreationDelegate.createValidateDependenciesTaskAction(), preparesForIndexing: false, enableSandboxing: false)
     }
 }
 

From 21ec8fe2707721094c3cb62ad8665ccbbdcb89b1 Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Tue, 5 Aug 2025 19:10:52 -0700
Subject: [PATCH 42/49] Remove an old ArgumentParser workaround

---
 Sources/SWBUtil/PbxCp.swift | 7 +------
 1 file changed, 1 insertion(+), 6 deletions(-)

diff --git a/Sources/SWBUtil/PbxCp.swift b/Sources/SWBUtil/PbxCp.swift
index 19c14c11..f40ad1fd 100644
--- a/Sources/SWBUtil/PbxCp.swift
+++ b/Sources/SWBUtil/PbxCp.swift
@@ -17,12 +17,7 @@
 
 import Foundation
 
-#if canImport(ArgumentParserInternal)
-    // remove once rdar://140831929 is resolved
-    public import ArgumentParserInternal
-#else
-    public import ArgumentParser
-#endif
+public import ArgumentParser
 
 import SWBCSupport
 import SWBLibc

From 8063c53e78dc0ea73827e068724054144888ad8c Mon Sep 17 00:00:00 2001
From: Bob Wilson <bob.wilson@apple.com>
Date: Tue, 29 Jul 2025 10:29:59 -0700
Subject: [PATCH 43/49] Add HEADER_DEPENDENCIES build setting with validation

This is similar to MODULE_DEPENDENCIES but for non-modular header file inclusion
dependencies. Our approach to this has changed a bit over the last few months,
and the trace output from Clang does not distinguish the modular vs. non-modular
includes. We will work on fixing that, but in the meantime, this continues the
placeholder approach used for MODULE_DEPENDENCIES where it assumes anything in a
framework has a module with the same name as the framework.

This also removes moduleDependenciesContext from ClangTaskPayload because the
code that needs that now gets it from ValidateDependenciesPayload.

rdar://150309197
---
 Sources/SWBCore/Dependencies.swift            | 202 +++++++++++++++---
 Sources/SWBCore/PlannedTaskAction.swift       |   1 +
 Sources/SWBCore/Settings/BuiltinMacros.swift  |   4 +
 Sources/SWBCore/Settings/Settings.swift       |   9 +
 .../PropertyDomainSpec.swift                  |   1 +
 .../SpecImplementations/Tools/CCompiler.swift |  32 +--
 .../Tools/ValidateDependencies.swift          |   6 +-
 Sources/SWBCore/Specs/CoreBuildSystem.xcspec  |  10 +
 .../Specs/en.lproj/CoreBuildSystem.strings    |   3 +
 Sources/SWBCore/TaskGeneration.swift          |   1 +
 .../SourcesTaskProducer.swift                 |   6 +-
 .../TaskProducers/TaskProducer.swift          |   2 +
 .../BuildDescriptionManager.swift             |   4 +
 .../BuiltinTaskActionsExtension.swift         |   1 +
 .../TaskActions/ClangCompileTaskAction.swift  | 108 +++++++++-
 .../ValidateDependenciesTaskAction.swift      |  69 +++---
 .../CapturingTaskGenerationDelegate.swift     |   4 +
 .../SWBTestSupport/DummyCommandProducer.swift |   4 +
 .../TaskPlanningTestSupport.swift             |   4 +
 Sources/SWBUniversalPlatform/Plugin.swift     |   2 +-
 .../DependencyValidationTests.swift           |  65 ++++++
 .../WatchBuildOperationTests.swift            |   1 +
 .../CommandLineSpecPerfTests.swift            |   4 +
 ...ncyVerificationTaskConstructionTests.swift |   8 +-
 24 files changed, 451 insertions(+), 100 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index 0675792a..5888602a 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -92,41 +92,13 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
         self.init(validate: validate, moduleDependencies: settings.moduleDependencies, fixItContext: fixItContext)
     }
 
-    /// Compute missing module dependencies from Clang imports.
-    ///
-    /// The compiler tracing information does not provide the import locations or whether they are public imports
-    /// (which depends on whether the import is in an installed header file).
-    /// If `files` is nil, the current toolchain does support the feature to trace imports.
-    public func computeMissingDependencies(files: [Path]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
-        guard validate != .no else { return [] }
-        guard let files else {
-            return nil
-        }
-
-        // The following is a provisional/incomplete mechanism for resolving a module dependency from a file path.
-        // For now, just grab the framework name and assume there is a module with the same name.
-        func findFrameworkName(_ file: Path) -> String? {
-            if file.fileExtension == "framework" {
-                return file.basenameWithoutSuffix
-            }
-            return file.dirname.isEmpty || file.dirname.isRoot ? nil : findFrameworkName(file.dirname)
-        }
-
-        let moduleDependencyNames = moduleDependencies.map { $0.name }
-        let fileNames = files.compactMap { findFrameworkName($0) }
-        let missingDeps = Set(fileNames.filter {
-            return !moduleDependencyNames.contains($0)
-        }.map {
-            ModuleDependency(name: $0, accessLevel: .Private)
-        })
-
-        return missingDeps.map { ($0, []) }
-    }
-
-    /// Compute missing module dependencies from Swift imports.
+    /// Compute missing module dependencies.
     ///
     /// If `imports` is nil, the current toolchain does not support the features to gather imports.
-    public func computeMissingDependencies(imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
+    public func computeMissingDependencies(
+        imports: [(ModuleDependency, importLocations: [Diagnostic.Location])]?,
+        fromSwift: Bool
+    ) -> [(ModuleDependency, importLocations: [Diagnostic.Location])]? {
         guard validate != .no else { return [] }
         guard let imports else {
             return nil
@@ -134,7 +106,7 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
 
         return imports.filter {
             // ignore module deps without source locations, these are inserted by swift / swift-build and we should treat them as implementation details which we can track without needing the user to declare them
-            if $0.importLocations.isEmpty { return false }
+            if fromSwift && $0.importLocations.isEmpty { return false }
 
             // TODO: if the difference is just the access modifier, we emit a new entry, but ultimately our fixit should update the existing entry or emit an error about a conflict
             if moduleDependencies.contains($0.0) { return false }
@@ -235,6 +207,166 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
     }
 }
 
+public struct HeaderDependency: Hashable, Sendable, SerializableCodable {
+    public let name: String
+    public let accessLevel: AccessLevel
+
+    public enum AccessLevel: String, Hashable, Sendable, CaseIterable, Codable, Serializable {
+        case Private = "private"
+        case Public = "public"
+
+        public init(_ string: String) throws {
+            guard let accessLevel = AccessLevel(rawValue: string) else {
+                throw StubError.error("unexpected access modifier '\(string)', expected one of: \(AccessLevel.allCases.map { $0.rawValue }.joined(separator: ", "))")
+            }
+
+            self = accessLevel
+        }
+    }
+
+    public init(name: String, accessLevel: AccessLevel) {
+        self.name = name
+        self.accessLevel = accessLevel
+    }
+
+    public init(entry: String) throws {
+        var it = entry.split(separator: " ").makeIterator()
+        switch (it.next(), it.next(), it.next()) {
+        case (let .some(name), nil, nil):
+            self.name = String(name)
+            self.accessLevel = .Private
+
+        case (let .some(accessLevel), let .some(name), nil):
+            self.name = String(name)
+            self.accessLevel = try AccessLevel(String(accessLevel))
+
+        default:
+            throw StubError.error("expected 1 or 2 space-separated components in: \(entry)")
+        }
+    }
+
+    public var asBuildSettingEntry: String {
+        "\(accessLevel == .Private ? "" : "\(accessLevel.rawValue) ")\(name)"
+    }
+
+    public var asBuildSettingEntryQuotedIfNeeded: String {
+        let e = asBuildSettingEntry
+        return e.contains(" ") ? "\"\(e)\"" : e
+    }
+}
+
+public struct HeaderDependenciesContext: Sendable, SerializableCodable {
+    public var validate: BooleanWarningLevel
+    var headerDependencies: [HeaderDependency]
+    var fixItContext: FixItContext?
+
+    init(validate: BooleanWarningLevel, headerDependencies: [HeaderDependency], fixItContext: FixItContext? = nil) {
+        self.validate = validate
+        self.headerDependencies = headerDependencies
+        self.fixItContext = fixItContext
+    }
+
+    public init?(settings: Settings) {
+        let validate = settings.globalScope.evaluate(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES)
+        guard validate != .no else { return nil }
+        let fixItContext = HeaderDependenciesContext.FixItContext(settings: settings)
+        self.init(validate: validate, headerDependencies: settings.headerDependencies, fixItContext: fixItContext)
+    }
+
+    /// Make diagnostics for missing header dependencies.
+    ///
+    /// The compiler tracing information does not provide the include locations or whether they are public imports
+    /// (which depends on whether the import is in an installed header file).
+    /// If `includes` is nil, the current toolchain does support the feature to trace imports.
+    public func makeDiagnostics(includes: [Path]?) -> [Diagnostic] {
+        guard validate != .no else { return [] }
+        guard let includes else {
+            return [Diagnostic(
+                behavior: .warning,
+                location: .unknown,
+                data: DiagnosticData("The current toolchain does not support \(BuiltinMacros.VALIDATE_HEADER_DEPENDENCIES.name)"))]
+        }
+
+        let headerDependencyNames = headerDependencies.map { $0.name }
+        let missingDeps = includes.filter { file in
+            return !headerDependencyNames.contains(where: { file.ends(with: $0) })
+        }.map {
+            // TODO: What if the basename doesn't uniquely identify the header?
+            HeaderDependency(name: $0.basename, accessLevel: .Private)
+        }
+
+        guard !missingDeps.isEmpty else { return [] }
+
+        let behavior: Diagnostic.Behavior = validate == .yesError ? .error : .warning
+
+        let fixIt = fixItContext?.makeFixIt(newHeaders: missingDeps)
+        let fixIts = fixIt.map { [$0] } ?? []
+
+        let message = "Missing entries in \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(missingDeps.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " "))"
+
+        let location: Diagnostic.Location = fixIt.map {
+            Diagnostic.Location.path($0.sourceRange.path, line: $0.sourceRange.endLine, column: $0.sourceRange.endColumn)
+        } ?? Diagnostic.Location.buildSetting(BuiltinMacros.HEADER_DEPENDENCIES)
+
+        return [Diagnostic(
+            behavior: behavior,
+            location: location,
+            data: DiagnosticData(message),
+            fixIts: fixIts)]
+    }
+
+    struct FixItContext: Sendable, SerializableCodable {
+        var sourceRange: Diagnostic.SourceRange
+        var modificationStyle: ModificationStyle
+
+        init(sourceRange: Diagnostic.SourceRange, modificationStyle: ModificationStyle) {
+            self.sourceRange = sourceRange
+            self.modificationStyle = modificationStyle
+        }
+
+        init?(settings: Settings) {
+            guard let target = settings.target else { return nil }
+            let thisTargetCondition = MacroCondition(parameter: BuiltinMacros.targetNameCondition, valuePattern: target.name)
+
+            if let assignment = (settings.globalScope.table.lookupMacro(BuiltinMacros.HEADER_DEPENDENCIES)?.sequence.first {
+                   $0.location != nil && ($0.conditions?.conditions == [thisTargetCondition] || ($0.conditions?.conditions.isEmpty ?? true))
+               }),
+               let location = assignment.location
+            {
+                self.init(sourceRange: .init(path: location.path, startLine: location.endLine, startColumn: location.endColumn, endLine: location.endLine, endColumn: location.endColumn), modificationStyle: .appendToExistingAssignment)
+            }
+            else if let path = settings.constructionComponents.targetXcconfigPath {
+                self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: nil))
+            }
+            else if let path = settings.constructionComponents.projectXcconfigPath {
+                self.init(sourceRange: .init(path: path, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max), modificationStyle: .insertNewAssignment(targetNameCondition: target.name))
+            }
+            else {
+                return nil
+            }
+        }
+
+        enum ModificationStyle: Sendable, SerializableCodable, Hashable {
+            case appendToExistingAssignment
+            case insertNewAssignment(targetNameCondition: String?)
+        }
+
+        func makeFixIt(newHeaders: [HeaderDependency]) -> Diagnostic.FixIt {
+            let stringValue = newHeaders.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " ")
+            let newText: String
+            switch modificationStyle {
+            case .appendToExistingAssignment:
+                newText = " \(stringValue)"
+            case .insertNewAssignment(let targetNameCondition):
+                let targetCondition = targetNameCondition.map { "[target=\($0)]" } ?? ""
+                newText = "\n\(BuiltinMacros.HEADER_DEPENDENCIES.name)\(targetCondition) = $(inherited) \(stringValue)\n"
+            }
+
+            return Diagnostic.FixIt(sourceRange: sourceRange, newText: newText)
+        }
+    }
+}
+
 public struct DependencyValidationInfo: Hashable, Sendable, Codable {
     public struct Import: Hashable, Sendable, Codable {
         public let dependency: ModuleDependency
@@ -247,7 +379,7 @@ public struct DependencyValidationInfo: Hashable, Sendable, Codable {
     }
 
     public enum Payload: Hashable, Sendable, Codable {
-        case clangDependencies(files: [String])
+        case clangDependencies(imports: [Import], includes: [Path])
         case swiftDependencies(imports: [Import])
         case unsupported
     }
diff --git a/Sources/SWBCore/PlannedTaskAction.swift b/Sources/SWBCore/PlannedTaskAction.swift
index 4844d09c..4ffc6484 100644
--- a/Sources/SWBCore/PlannedTaskAction.swift
+++ b/Sources/SWBCore/PlannedTaskAction.swift
@@ -337,6 +337,7 @@ public protocol TaskActionCreationDelegate
     func createValidateProductTaskAction() -> any PlannedTaskAction
     func createConstructStubExecutorInputFileListTaskAction() -> any PlannedTaskAction
     func createClangCompileTaskAction() -> any PlannedTaskAction
+    func createClangNonModularCompileTaskAction() -> any PlannedTaskAction
     func createClangScanTaskAction() -> any PlannedTaskAction
     func createSwiftDriverTaskAction() -> any PlannedTaskAction
     func createSwiftCompilationRequirementTaskAction() -> any PlannedTaskAction
diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift
index 5a90b641..d0f5dbd5 100644
--- a/Sources/SWBCore/Settings/BuiltinMacros.swift
+++ b/Sources/SWBCore/Settings/BuiltinMacros.swift
@@ -732,6 +732,7 @@ public final class BuiltinMacros {
     public static let GLOBAL_CFLAGS = BuiltinMacros.declareStringListMacro("GLOBAL_CFLAGS")
     public static let HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_TARGETS_NOT_BEING_BUILT = BuiltinMacros.declareBooleanMacro("HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_TARGETS_NOT_BEING_BUILT")
     public static let HEADERMAP_USES_VFS = BuiltinMacros.declareBooleanMacro("HEADERMAP_USES_VFS")
+    public static let HEADER_DEPENDENCIES = BuiltinMacros.declareStringListMacro("HEADER_DEPENDENCIES")
     public static let HEADER_OUTPUT_DIR = BuiltinMacros.declareStringMacro("HEADER_OUTPUT_DIR")
     public static let HEADER_SEARCH_PATHS = BuiltinMacros.declarePathListMacro("HEADER_SEARCH_PATHS")
     public static let IBC_REGIONS_AND_STRINGS_FILES = BuiltinMacros.declareStringListMacro("IBC_REGIONS_AND_STRINGS_FILES")
@@ -1145,6 +1146,7 @@ public final class BuiltinMacros {
     public static let VALIDATE_PRODUCT = BuiltinMacros.declareBooleanMacro("VALIDATE_PRODUCT")
     public static let VALIDATE_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
     public static let VALIDATE_DEVELOPMENT_ASSET_PATHS = BuiltinMacros.declareEnumMacro("VALIDATE_DEVELOPMENT_ASSET_PATHS") as EnumMacroDeclaration<BooleanWarningLevel>
+    public static let VALIDATE_HEADER_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_HEADER_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
     public static let VALIDATE_MODULE_DEPENDENCIES = BuiltinMacros.declareEnumMacro("VALIDATE_MODULE_DEPENDENCIES") as EnumMacroDeclaration<BooleanWarningLevel>
     public static let VECTOR_SUFFIX = BuiltinMacros.declareStringMacro("VECTOR_SUFFIX")
     public static let VERBOSE_PBXCP = BuiltinMacros.declareBooleanMacro("VERBOSE_PBXCP")
@@ -1798,6 +1800,7 @@ public final class BuiltinMacros {
         GROUP,
         HEADERMAP_INCLUDES_FRAMEWORK_ENTRIES_FOR_TARGETS_NOT_BEING_BUILT,
         HEADERMAP_USES_VFS,
+        HEADER_DEPENDENCIES,
         HEADER_SEARCH_PATHS,
         HEADER_OUTPUT_DIR,
         HOME,
@@ -2373,6 +2376,7 @@ public final class BuiltinMacros {
         VALIDATE_PRODUCT,
         VALIDATE_DEPENDENCIES,
         VALIDATE_DEVELOPMENT_ASSET_PATHS,
+        VALIDATE_HEADER_DEPENDENCIES,
         VALIDATE_MODULE_DEPENDENCIES,
         VALID_ARCHS,
         VECTOR_SUFFIX,
diff --git a/Sources/SWBCore/Settings/Settings.swift b/Sources/SWBCore/Settings/Settings.swift
index bf5923f6..fc4b362d 100644
--- a/Sources/SWBCore/Settings/Settings.swift
+++ b/Sources/SWBCore/Settings/Settings.swift
@@ -756,6 +756,7 @@ public final class Settings: PlatformBuildContext, Sendable {
     }
 
     public let moduleDependencies: [ModuleDependency]
+    public let headerDependencies: [HeaderDependency]
 
     public static func supportsMacCatalyst(scope: MacroEvaluationScope, core: Core) -> Bool {
         @preconcurrency @PluginExtensionSystemActor func sdkVariantInfoExtensions() -> [any SDKVariantInfoExtensionPoint.ExtensionProtocol] {
@@ -904,6 +905,7 @@ public final class Settings: PlatformBuildContext, Sendable {
 
         self.supportedBuildVersionPlatforms = effectiveSupportedPlatforms(sdkRegistry: sdkRegistry)
         self.moduleDependencies = builder.moduleDependencies
+        self.headerDependencies = builder.headerDependencies
 
         self.constructionComponents = builder.constructionComponents
     }
@@ -1290,6 +1292,7 @@ private class SettingsBuilder {
     var signingSettings: Settings.SigningSettings? = nil
 
     var moduleDependencies: [ModuleDependency] = []
+    var headerDependencies: [HeaderDependency] = []
 
 
     // Mutable state of the builder as we're building up the settings table.
@@ -1631,6 +1634,12 @@ private class SettingsBuilder {
         catch {
             errors.append("Failed to parse \(BuiltinMacros.MODULE_DEPENDENCIES.name): \(error)")
         }
+        do {
+            self.headerDependencies = try createScope(sdkToUse: boundProperties.sdk).evaluate(BuiltinMacros.HEADER_DEPENDENCIES).map { try HeaderDependency(entry: $0) }
+        }
+        catch {
+            errors.append("Failed to parse \(BuiltinMacros.HEADER_DEPENDENCIES.name): \(error)")
+        }
 
         // At this point settings construction is finished.
 
diff --git a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
index fe284753..0738bac6 100644
--- a/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
+++ b/Sources/SWBCore/SpecImplementations/PropertyDomainSpec.swift
@@ -101,6 +101,7 @@ private final class EnumBuildOptionType : BuildOptionType {
             return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<PackageResourceTargetKind>
         case "VALIDATE_DEPENDENCIES",
             "VALIDATE_MODULE_DEPENDENCIES",
+            "VALIDATE_HEADER_DEPENDENCIES",
             "VALIDATE_DEVELOPMENT_ASSET_PATHS":
             return try namespace.declareEnumMacro(name) as EnumMacroDeclaration<BooleanWarningLevel>
         case "STRIP_STYLE":
diff --git a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
index 5cba1b9c..d09d9119 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/CCompiler.swift
@@ -432,11 +432,10 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
 
     public let fileNameMapPath: Path?
 
-    public let moduleDependenciesContext: ModuleDependenciesContext?
     public let traceFilePath: Path?
     public let dependencyValidationOutputPath: Path?
 
-    fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, moduleDependenciesContext: ModuleDependenciesContext? = nil, traceFilePath: Path? = nil, dependencyValidationOutputPath: Path? = nil) {
+    fileprivate init(serializedDiagnosticsPath: Path?, indexingPayload: ClangIndexingPayload?, explicitModulesPayload: ClangExplicitModulesPayload? = nil, outputObjectFilePath: Path? = nil, fileNameMapPath: Path? = nil, developerPathString: String? = nil, traceFilePath: Path? = nil, dependencyValidationOutputPath: Path? = nil) {
         if let developerPathString, explicitModulesPayload == nil {
             self.dependencyInfoEditPayload = .init(removablePaths: [], removableBasenames: [], developerPath: Path(developerPathString))
         } else {
@@ -447,34 +446,31 @@ public struct ClangTaskPayload: ClangModuleVerifierPayloadType, DependencyInfoEd
         self.explicitModulesPayload = explicitModulesPayload
         self.outputObjectFilePath = outputObjectFilePath
         self.fileNameMapPath = fileNameMapPath
-        self.moduleDependenciesContext = moduleDependenciesContext
         self.traceFilePath = traceFilePath
         self.dependencyValidationOutputPath = dependencyValidationOutputPath
     }
 
     public func serialize<T: Serializer>(to serializer: T) {
-        serializer.serializeAggregate(9) {
+        serializer.serializeAggregate(8) {
             serializer.serialize(serializedDiagnosticsPath)
             serializer.serialize(indexingPayload)
             serializer.serialize(explicitModulesPayload)
             serializer.serialize(outputObjectFilePath)
             serializer.serialize(fileNameMapPath)
             serializer.serialize(dependencyInfoEditPayload)
-            serializer.serialize(moduleDependenciesContext)
             serializer.serialize(traceFilePath)
             serializer.serialize(dependencyValidationOutputPath)
         }
     }
 
     public init(from deserializer: any Deserializer) throws {
-        try deserializer.beginAggregate(9)
+        try deserializer.beginAggregate(8)
         self.serializedDiagnosticsPath = try deserializer.deserialize()
         self.indexingPayload = try deserializer.deserialize()
         self.explicitModulesPayload = try deserializer.deserialize()
         self.outputObjectFilePath = try deserializer.deserialize()
         self.fileNameMapPath = try deserializer.deserialize()
         self.dependencyInfoEditPayload = try deserializer.deserialize()
-        self.moduleDependenciesContext = try deserializer.deserialize()
         self.traceFilePath = try deserializer.deserialize()
         self.dependencyValidationOutputPath = try deserializer.deserialize()
     }
@@ -1183,12 +1179,14 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
 
         let extraOutputs: [any PlannedNode]
         let moduleDependenciesContext = cbc.producer.moduleDependenciesContext
+        let headerDependenciesContext = cbc.producer.headerDependenciesContext
         let dependencyValidationOutputPath: Path?
         let traceFilePath: Path?
         if clangInfo?.hasFeature("print-headers-direct-per-file") ?? false,
-            (moduleDependenciesContext?.validate ?? .defaultValue) != .no {
-            dependencyValidationOutputPath = Path(outputNode.path.str + ".dependencies")
+            ((moduleDependenciesContext?.validate ?? .defaultValue) != .no ||
+             (headerDependenciesContext?.validate ?? .defaultValue) != .no) {
 
+            dependencyValidationOutputPath = Path(outputNode.path.str + ".dependencies")
             let file = Path(outputNode.path.str + ".trace.json")
             commandLine += [
                 "-Xclang", "-header-include-file",
@@ -1316,7 +1314,6 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
             outputObjectFilePath: shouldGenerateRemarks ? outputNode.path : nil,
             fileNameMapPath: verifierPayload?.fileNameMapPath,
             developerPathString: recordSystemHeaderDepsOutsideSysroot ? cbc.scope.evaluate(BuiltinMacros.DEVELOPER_DIR).str : nil,
-            moduleDependenciesContext: moduleDependenciesContext,
             traceFilePath: traceFilePath,
             dependencyValidationOutputPath: dependencyValidationOutputPath
         )
@@ -1368,21 +1365,8 @@ public class ClangCompilerSpec : CompilerSpec, SpecIdentifierType, GCCCompatible
             extraInputs = []
         }
 
-        if let moduleDependenciesContext {
-            do {
-                let jsonData = try JSONEncoder(outputFormatting: [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]).encode(moduleDependenciesContext)
-                guard let signature = String(data: jsonData, encoding: .utf8) else {
-                    throw StubError.error("non-UTF-8 data")
-                }
-                additionalSignatureData += "|\(signature)"
-            } catch {
-                delegate.error("failed to serialize 'MODULE_DEPENDENCIES' context information: \(error)")
-                return
-            }
-        }
-
         // Finally, create the task.
-        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs + additionalOutputs.outputs.map { delegate.createNode($0.path) }, action: action ?? delegate.taskActionCreationDelegate.createDeferredExecutionTaskActionIfRequested(userPreferences: cbc.producer.userPreferences), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
+        delegate.createTask(type: self, dependencyData: dependencyData, payload: payload, ruleInfo: ruleInfo, additionalSignatureData: additionalSignatureData, commandLine: commandLine, additionalOutput: additionalOutput, environment: environmentBindings, workingDirectory: compilerWorkingDirectory(cbc), inputs: inputNodes + extraInputs, outputs: [outputNode] + extraOutputs + additionalOutputs.outputs.map { delegate.createNode($0.path) }, action: action ?? delegate.taskActionCreationDelegate.createClangNonModularCompileTaskAction(), execDescription: resolveExecutionDescription(cbc, delegate), enableSandboxing: enableSandboxing, additionalTaskOrderingOptions: [.compilationForIndexableSourceFile], usesExecutionInputs: usesExecutionInputs, showEnvironment: true, priority: .preferred)
 
         // If the object file verifier is enabled and we are building with explicit modules, also create a job to produce adjacent objects using implicit modules, then compare the results.
         if cbc.scope.evaluate(BuiltinMacros.CLANG_ENABLE_EXPLICIT_MODULES_OBJECT_FILE_VERIFIER) && action != nil {
diff --git a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
index 43021306..9407cd11 100644
--- a/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
+++ b/Sources/SWBCore/SpecImplementations/Tools/ValidateDependencies.swift
@@ -55,9 +55,11 @@ public final class ValidateDependenciesSpec: CommandLineToolSpec, SpecImplementa
 }
 
 public struct ValidateDependenciesPayload: TaskPayload, Sendable, SerializableCodable {
-    public let moduleDependenciesContext: ModuleDependenciesContext
+    public let moduleDependenciesContext: ModuleDependenciesContext?
+    public let headerDependenciesContext: HeaderDependenciesContext?
 
-    public init(moduleDependenciesContext: ModuleDependenciesContext) {
+    public init(moduleDependenciesContext: ModuleDependenciesContext?, headerDependenciesContext: HeaderDependenciesContext?) {
         self.moduleDependenciesContext = moduleDependenciesContext
+        self.headerDependenciesContext = headerDependenciesContext
     }
 }
diff --git a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
index fe11ab15..e2c35411 100644
--- a/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
+++ b/Sources/SWBCore/Specs/CoreBuildSystem.xcspec
@@ -1611,6 +1611,16 @@ When `GENERATE_INFOPLIST_FILE` is enabled, sets the value of the [CFBundleIdenti
                     sdk,
                 );
             },
+            {
+                Name = "HEADER_DEPENDENCIES";
+                Type = StringList;
+                Category = BuildOptions;
+                DefaultValue = "";
+                ConditionFlavors = (
+                    arch,
+                    sdk,
+                );
+            },
             {
                 Name = "GENERATE_PRELINK_OBJECT_FILE";
                 Type = Boolean;
diff --git a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
index fef09bcc..769350c7 100644
--- a/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
+++ b/Sources/SWBCore/Specs/en.lproj/CoreBuildSystem.strings
@@ -400,6 +400,9 @@ Generally you should not specify an order file in Debug or Development configura
 "[MODULE_DEPENDENCIES]-name" = "Module Dependencies";
 "[MODULE_DEPENDENCIES]-description" = "Other modules this target depends on.";
 
+"[HEADER_DEPENDENCIES]-name" = "Header Dependencies";
+"[HEADER_DEPENDENCIES]-description" = "Header files this target depends on.";
+
 "[OTHER_LIBTOOLFLAGS]-name" = "Other Librarian Flags";
 "[OTHER_LIBTOOLFLAGS]-description" = "Options defined in this setting are passed to all invocations of the archive librarian, which is used to generate static libraries.";
 
diff --git a/Sources/SWBCore/TaskGeneration.swift b/Sources/SWBCore/TaskGeneration.swift
index 6bba61ec..7ef96613 100644
--- a/Sources/SWBCore/TaskGeneration.swift
+++ b/Sources/SWBCore/TaskGeneration.swift
@@ -273,6 +273,7 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
     var hostOperatingSystem: OperatingSystem { get }
 
     var moduleDependenciesContext: ModuleDependenciesContext? { get }
+    var headerDependenciesContext: HeaderDependenciesContext? { get }
 }
 
 extension CommandProducer {
diff --git a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
index 8a4ff447..c02f73e2 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/BuildPhaseTaskProducers/SourcesTaskProducer.swift
@@ -1574,14 +1574,16 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
         }
 
         // Create a task to validate dependencies if that feature is enabled.
-        if let moduleDependenciesContext = context.moduleDependenciesContext, moduleDependenciesContext.validate != .no {
+        let validateModuleDeps = (context.moduleDependenciesContext?.validate ?? .no) != .no
+        let validateHeaderDeps = (context.headerDependenciesContext?.validate ?? .no) != .no
+        if validateModuleDeps || validateHeaderDeps {
             var validateDepsTasks = [any PlannedTask]()
             await appendGeneratedTasks(&validateDepsTasks, usePhasedOrdering: true) { delegate in
                 await context.validateDependenciesSpec.createTasks(
                     CommandBuildContext(producer: context, scope: scope, inputs: []),
                     delegate,
                     dependencyInfos: dependencyDataFiles,
-                    payload: .init(moduleDependenciesContext: moduleDependenciesContext)
+                    payload: .init(moduleDependenciesContext: context.moduleDependenciesContext, headerDependenciesContext: context.headerDependenciesContext)
                 )
             }
             tasks.append(contentsOf: validateDepsTasks)
diff --git a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
index b24fbc1c..d5397dac 100644
--- a/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
+++ b/Sources/SWBTaskConstruction/TaskProducers/TaskProducer.swift
@@ -122,6 +122,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
     var emitFrontendCommandLines: Bool
 
     public let moduleDependenciesContext: ModuleDependenciesContext?
+    public let headerDependenciesContext: HeaderDependenciesContext?
 
     private struct State: Sendable {
         fileprivate var onDemandResourcesAssetPacks: [ODRTagSet: ODRAssetPackInfo] = [:]
@@ -439,6 +440,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
         }
 
         self.moduleDependenciesContext = ModuleDependenciesContext(settings: settings)
+        self.headerDependenciesContext = HeaderDependenciesContext(settings: settings)
     }
 
     /// The set of all known deployment target macro names, even if the platforms that use those settings are not installed.
diff --git a/Sources/SWBTaskExecution/BuildDescriptionManager.swift b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
index 05e43c74..ecdfbbbd 100644
--- a/Sources/SWBTaskExecution/BuildDescriptionManager.swift
+++ b/Sources/SWBTaskExecution/BuildDescriptionManager.swift
@@ -856,6 +856,10 @@ extension BuildSystemTaskPlanningDelegate: TaskActionCreationDelegate {
         return ClangCompileTaskAction()
     }
 
+    func createClangNonModularCompileTaskAction() -> any PlannedTaskAction {
+        return ClangNonModularCompileTaskAction()
+    }
+
     func createClangScanTaskAction() -> any PlannedTaskAction {
         return ClangScanTaskAction()
     }
diff --git a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
index cd19b016..1b2e6312 100644
--- a/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
+++ b/Sources/SWBTaskExecution/BuiltinTaskActionsExtension.swift
@@ -53,6 +53,7 @@ public struct BuiltinTaskActionsExtension: TaskActionExtension {
             38: GenericCachingTaskAction.self,
             39: ProcessSDKImportsTaskAction.self,
             40: ValidateDependenciesTaskAction.self,
+            41: ClangNonModularCompileTaskAction.self,
             42: ObjectLibraryAssemblerTaskAction.self,
             43: LinkerTaskAction.self,
         ]
diff --git a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
index 88062d09..10941936 100644
--- a/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/ClangCompileTaskAction.swift
@@ -250,15 +250,12 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
 
             // Check if verifying dependencies from trace data is enabled.
             let traceFilePath: Path?
-            let moduleDependenciesContext: ModuleDependenciesContext?
             let dependencyValidationOutputPath: Path?
             if let payload = task.payload as? ClangTaskPayload {
                 traceFilePath = payload.traceFilePath
-                moduleDependenciesContext = payload.moduleDependenciesContext
                 dependencyValidationOutputPath = payload.dependencyValidationOutputPath
             } else {
                 traceFilePath = nil
-                moduleDependenciesContext = nil
                 dependencyValidationOutputPath = nil
             }
             if let traceFilePath {
@@ -334,7 +331,8 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
 
                     var allFiles = Set<Path>()
                     traceData.forEach { allFiles.formUnion(Set($0.includes)) }
-                    payload = .clangDependencies(files: allFiles.map { $0.str })
+                    let (imports, includes) = separateImportsFromIncludes(allFiles)
+                    payload = .clangDependencies(imports: imports, includes: includes)
                 } else {
                     payload = .unsupported
                 }
@@ -357,6 +355,31 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
         }
     }
 
+    // Clang's dependency tracing does not currently clearly distinguish modular imports from non-modular includes.
+    // Until that gets fixed, just guess that if the file is contained in a framework, it comes from a module with
+    // the same name. That is obviously not going to be reliable but it unblocks us from continuing experiments with
+    // dependency specifications.
+    private func separateImportsFromIncludes(_ files: Set<Path>) -> ([DependencyValidationInfo.Import], [Path]) {
+        func findFrameworkName(_ file: Path) -> String? {
+            if file.fileExtension == "framework" {
+                return file.basenameWithoutSuffix
+            }
+            return file.dirname.isEmpty || file.dirname.isRoot ? nil : findFrameworkName(file.dirname)
+        }
+        var moduleNames: [String] = []
+        var includeFiles: [Path] = []
+        for file in files {
+            if let frameworkName = findFrameworkName(file) {
+                moduleNames.append(frameworkName)
+            } else {
+                includeFiles.append(file)
+            }
+        }
+        let moduleDependencies = moduleNames.map { ModuleDependency(name: $0, accessLevel: .Private) }
+        let moduleImports = moduleDependencies.map { DependencyValidationInfo.Import(dependency: $0, importLocations: []) }
+        return (moduleImports, includeFiles)
+    }
+
     /// Intended to be called during task dependency setup.
     /// If remote caching is enabled along with integrated cache queries, it will request
     /// a `ClangCachingMaterializeKeyTaskAction` as task dependency.
@@ -478,6 +501,83 @@ public final class ClangCompileTaskAction: TaskAction, BuildValueValidatingTaskA
     }
 }
 
+public final class ClangNonModularCompileTaskAction: TaskAction {
+    public override class var toolIdentifier: String {
+        return "ccompile-nonmodular"
+    }
+
+    override public func performTaskAction(
+        _ task: any ExecutableTask,
+        dynamicExecutionDelegate: any DynamicTaskExecutionDelegate,
+        executionDelegate: any TaskExecutionDelegate,
+        clientDelegate: any TaskExecutionClientDelegate,
+        outputDelegate: any TaskOutputDelegate,
+    ) async -> CommandResult {
+        do {
+            // Check if verifying dependencies from trace data is enabled.
+            let traceFilePath: Path?
+            let dependencyValidationOutputPath: Path?
+            if let payload = task.payload as? ClangTaskPayload {
+                traceFilePath = payload.traceFilePath
+                dependencyValidationOutputPath = payload.dependencyValidationOutputPath
+            } else {
+                traceFilePath = nil
+                dependencyValidationOutputPath = nil
+            }
+            if let traceFilePath {
+                // Remove the trace output file if it already exists.
+                if executionDelegate.fs.exists(traceFilePath) {
+                    try executionDelegate.fs.remove(traceFilePath)
+                }
+            }
+
+            let processDelegate = TaskProcessDelegate(outputDelegate: outputDelegate)
+            try await spawn(
+                commandLine: Array(task.commandLineAsStrings),
+                environment: task.environment.bindingsDictionary,
+                workingDirectory: task.workingDirectory,
+                dynamicExecutionDelegate: dynamicExecutionDelegate,
+                clientDelegate: clientDelegate,
+                processDelegate: processDelegate,
+            )
+            if let error = processDelegate.executionError {
+                outputDelegate.error(error)
+                return .failed
+            }
+            let execResult = processDelegate.commandResult ?? .failed
+
+            if execResult == .succeeded {
+                let payload: DependencyValidationInfo.Payload
+                if let traceFilePath {
+                    let fs = executionDelegate.fs
+                    let traceData = try JSONDecoder().decode(Array<TraceData>.self, from: fs.readMemoryMapped(traceFilePath))
+
+                    var allFiles = Set<Path>()
+                    traceData.forEach { allFiles.formUnion(Set($0.includes)) }
+                    payload = .clangDependencies(imports: [], includes: Array(allFiles))
+                } else {
+                    payload = .unsupported
+                }
+
+                if let dependencyValidationOutputPath {
+                    let validationInfo = DependencyValidationInfo(payload: payload)
+                    _ = try executionDelegate.fs.writeIfChanged(
+                        dependencyValidationOutputPath,
+                        contents: ByteString(
+                            JSONEncoder(outputFormatting: .sortedKeys).encode(validationInfo)
+                        )
+                    )
+                }
+            }
+
+            return execResult
+        } catch {
+            outputDelegate.error(error.localizedDescription)
+            return .failed
+        }
+    }
+}
+
 // Results from tracing header includes with "direct-per-file" filtering.
 // This is used to validate dependencies.
 fileprivate struct TraceData: Decodable {
diff --git a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
index 06f67771..9d463104 100644
--- a/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/ValidateDependenciesTaskAction.swift
@@ -26,7 +26,7 @@ public final class ValidateDependenciesTaskAction: TaskAction {
             return .failed
         }
 
-        guard let context = (task.payload as? ValidateDependenciesPayload)?.moduleDependenciesContext else {
+        guard let payload = (task.payload as? ValidateDependenciesPayload) else {
             if let payload = task.payload {
                 outputDelegate.emitError("invalid task payload: \(payload)")
             } else {
@@ -36,7 +36,8 @@ public final class ValidateDependenciesTaskAction: TaskAction {
         }
 
         do {
-            var allClangFiles = Set<String>()
+            var allClangIncludes = Set<Path>()
+            var allClangImports = Set<DependencyValidationInfo.Import>()
             var allSwiftImports = Set<DependencyValidationInfo.Import>()
             var unsupported = false
 
@@ -45,9 +46,12 @@ public final class ValidateDependenciesTaskAction: TaskAction {
                 let info = try JSONDecoder().decode(DependencyValidationInfo.self, from: inputData)
 
                 switch info.payload {
-                case .clangDependencies(let files):
-                    files.forEach {
-                        allClangFiles.insert($0)
+                case .clangDependencies(let imports, let includes):
+                    imports.forEach {
+                        allClangImports.insert($0)
+                    }
+                    includes.forEach {
+                        allClangIncludes.insert($0)
                     }
                 case .swiftDependencies(let imports):
                     imports.forEach {
@@ -60,33 +64,42 @@ public final class ValidateDependenciesTaskAction: TaskAction {
 
             var diagnostics: [Diagnostic] = []
 
-            if unsupported {
-                diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: nil))
-            } else {
-                let clangMissingDeps = context.computeMissingDependencies(files: allClangFiles.map { Path($0) })
-                let swiftMissingDeps = context.computeMissingDependencies(imports: allSwiftImports.map { ($0.dependency, $0.importLocations) })
+            if let moduleContext = payload.moduleDependenciesContext {
+                if unsupported {
+                    diagnostics.append(contentsOf: moduleContext.makeDiagnostics(missingDependencies: nil))
+                } else {
+                    let clangMissingDeps = moduleContext.computeMissingDependencies(imports: allClangImports.map { ($0.dependency, $0.importLocations) }, fromSwift: false)
+                    let swiftMissingDeps = moduleContext.computeMissingDependencies(imports: allSwiftImports.map { ($0.dependency, $0.importLocations) }, fromSwift: true)
 
-                // Update Swift dependencies with information from Clang dependencies on the same module.
-                var clangMissingDepsByName = [String: (ModuleDependency, importLocations: [Diagnostic.Location])]()
-                clangMissingDeps?.forEach {
-                    clangMissingDepsByName[$0.0.name] = $0
-                }
-                let updatedSwiftMissingDeps: [(ModuleDependency, importLocations: [Diagnostic.Location])] = swiftMissingDeps?.map {
-                    if let clangMissingDep = clangMissingDepsByName[$0.0.name] {
-                        return (
-                            ModuleDependency(name: $0.0.name, accessLevel: max($0.0.accessLevel, clangMissingDep.0.accessLevel)),
-                            $0.importLocations + clangMissingDep.importLocations
-                        )
-                    } else {
-                        return $0
+                    // Update Swift dependencies with information from Clang dependencies on the same module.
+                    var clangMissingDepsByName = [String: (ModuleDependency, importLocations: [Diagnostic.Location])]()
+                    clangMissingDeps?.forEach {
+                        clangMissingDepsByName[$0.0.name] = $0
                     }
-                } ?? []
+                    let updatedSwiftMissingDeps: [(ModuleDependency, importLocations: [Diagnostic.Location])] = swiftMissingDeps?.map {
+                        if let clangMissingDep = clangMissingDepsByName[$0.0.name] {
+                            return (
+                                ModuleDependency(name: $0.0.name, accessLevel: max($0.0.accessLevel, clangMissingDep.0.accessLevel)),
+                                $0.importLocations + clangMissingDep.importLocations
+                            )
+                        } else {
+                            return $0
+                        }
+                    } ?? []
 
-                // Filter missing C dependencies by known Swift dependencies to avoid duplicate diagnostics.
-                let swiftImports = Set(allSwiftImports.map { $0.dependency.name })
-                let uniqueClangMissingDeps = clangMissingDeps?.filter { !swiftImports.contains($0.0.name) } ?? []
+                    // Filter missing C dependencies by known Swift dependencies to avoid duplicate diagnostics.
+                    let swiftImports = Set(allSwiftImports.map { $0.dependency.name })
+                    let uniqueClangMissingDeps = clangMissingDeps?.filter { !swiftImports.contains($0.0.name) } ?? []
 
-                diagnostics.append(contentsOf: context.makeDiagnostics(missingDependencies: uniqueClangMissingDeps + updatedSwiftMissingDeps))
+                    diagnostics.append(contentsOf: moduleContext.makeDiagnostics(missingDependencies: uniqueClangMissingDeps + updatedSwiftMissingDeps))
+                }
+            }
+            if let headerContext = payload.headerDependenciesContext {
+                if unsupported {
+                    diagnostics.append(contentsOf: headerContext.makeDiagnostics(includes: nil))
+                } else {
+                    diagnostics.append(contentsOf: headerContext.makeDiagnostics(includes: Array(allClangIncludes)))
+                }
             }
 
             for diagnostic in diagnostics {
diff --git a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
index 979e947f..91f6937a 100644
--- a/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
+++ b/Sources/SWBTestSupport/CapturingTaskGenerationDelegate.swift
@@ -204,6 +204,10 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
         return ClangCompileTaskAction()
     }
 
+    package func createClangNonModularCompileTaskAction() -> any PlannedTaskAction {
+        return ClangNonModularCompileTaskAction()
+    }
+
     package func createClangScanTaskAction() -> any PlannedTaskAction {
         return ClangScanTaskAction()
     }
diff --git a/Sources/SWBTestSupport/DummyCommandProducer.swift b/Sources/SWBTestSupport/DummyCommandProducer.swift
index 9d00f0b5..1b0e9e9a 100644
--- a/Sources/SWBTestSupport/DummyCommandProducer.swift
+++ b/Sources/SWBTestSupport/DummyCommandProducer.swift
@@ -244,4 +244,8 @@ package struct MockCommandProducer: CommandProducer, Sendable {
     package var moduleDependenciesContext: SWBCore.ModuleDependenciesContext? {
         nil
     }
+
+    package var headerDependenciesContext: SWBCore.HeaderDependenciesContext? {
+        nil
+    }
 }
diff --git a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
index cc6505df..9ba7c3d9 100644
--- a/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
+++ b/Sources/SWBTestSupport/TaskPlanningTestSupport.swift
@@ -432,6 +432,10 @@ extension TestTaskPlanningDelegate: TaskActionCreationDelegate {
         return ClangCompileTaskAction()
     }
 
+    package func createClangNonModularCompileTaskAction() -> any PlannedTaskAction {
+        return ClangNonModularCompileTaskAction()
+    }
+
     package func createClangScanTaskAction() -> any PlannedTaskAction {
         return ClangScanTaskAction()
     }
diff --git a/Sources/SWBUniversalPlatform/Plugin.swift b/Sources/SWBUniversalPlatform/Plugin.swift
index e6c44e50..4fffa083 100644
--- a/Sources/SWBUniversalPlatform/Plugin.swift
+++ b/Sources/SWBUniversalPlatform/Plugin.swift
@@ -81,6 +81,6 @@ struct UniversalPlatformTaskProducerExtension: TaskProducerExtension {
 
 struct UniversalPlatformTaskActionExtension: TaskActionExtension {
     var taskActionImplementations: [SWBUtil.SerializableTypeCode : any SWBUtil.PolymorphicSerializable.Type] {
-        [41: TestEntryPointGenerationTaskAction.self]
+        [44: TestEntryPointGenerationTaskAction.self]
     }
 }
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 3fd4b2cd..9a5955e9 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -620,4 +620,69 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.host), .requireClangFeatures(.printHeadersDirectPerFile), arguments: ["YES", "NO"])
+    func validateHeaderDependencies(enableModules: String) async throws {
+        try await withTemporaryDirectory { tmpDir async throws -> Void in
+            let testWorkspace = TestWorkspace(
+                "Test",
+                sourceRoot: tmpDir.join("Test"),
+                projects: [
+                    TestProject(
+                        "aProject",
+                        groupTree: TestGroup(
+                            "Sources", path: "Sources",
+                            children: [
+                                TestFile("CoreFoo.c")
+                            ]),
+                        buildConfigurations: [
+                            TestBuildConfiguration(
+                                "Debug",
+                                buildSettings: [
+                                    "PRODUCT_NAME": "$(TARGET_NAME)",
+                                    "CLANG_ENABLE_MODULES": enableModules,
+                                    "CLANG_ENABLE_EXPLICIT_MODULES": enableModules,
+                                    "GENERATE_INFOPLIST_FILE": "YES",
+                                    "HEADER_DEPENDENCIES": "stdio.h",
+                                    "VALIDATE_HEADER_DEPENDENCIES": "YES_ERROR",
+                                    "SDKROOT": "$(HOST_PLATFORM)",
+                                    "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
+                                    "DSTROOT": tmpDir.join("dstroot").str,
+                                ]
+                            )
+                        ],
+                        targets: [
+                            TestStandardTarget(
+                                "CoreFoo", type: .framework,
+                                buildPhases: [
+                                    TestSourcesBuildPhase(["CoreFoo.c"]),
+                                    TestFrameworksBuildPhase()
+                                ])
+                        ])
+                ]
+            )
+
+            let tester = try await BuildOperationTester(getCore(), testWorkspace, simulated: false)
+            let SRCROOT = testWorkspace.sourceRoot.join("aProject")
+
+            // Write the source files.
+            try await tester.fs.writeFileContents(SRCROOT.join("Sources/CoreFoo.c")) { contents in
+                contents <<< """
+                    #include <stdio.h>
+                    #include <stdlib.h>
+
+                    void f0(void) { };
+                """
+            }
+
+            // Expect complaint about undeclared dependency
+            try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug"), runDestination: .host, persistent: true) { results in
+                results.checkError(.contains("Missing entries in HEADER_DEPENDENCIES: stdlib.h"))
+            }
+
+            // Declaring dependencies resolves the problem
+            try await tester.checkBuild(parameters: BuildParameters(configuration: "Debug", overrides: ["HEADER_DEPENDENCIES": "stdio.h stdlib.h"]), runDestination: .host, persistent: true) { results in
+                results.checkNoErrors()
+            }
+        }
+    }
 }
diff --git a/Tests/SWBBuildSystemTests/WatchBuildOperationTests.swift b/Tests/SWBBuildSystemTests/WatchBuildOperationTests.swift
index be5d8045..f1206cfb 100644
--- a/Tests/SWBBuildSystemTests/WatchBuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/WatchBuildOperationTests.swift
@@ -157,6 +157,7 @@ fileprivate struct WatchBuildOperationTests: CoreBasedTests {
                 }
                 results.checkWarning(.contains("Could not read serialized diagnostics file"))
                 results.checkError(.equal("[targetIntegrity] Watch-Only Application Stubs are not available when building for watchOS. (in target 'Watchable' from project 'aProject')"))
+                results.checkWarning(.contains("Unable to perform dependency info modifications"))
                 results.checkNoDiagnostics()
             }
         }
diff --git a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
index 75c1d8ea..a9e2004d 100644
--- a/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
+++ b/Tests/SWBCorePerfTests/CommandLineSpecPerfTests.swift
@@ -198,6 +198,10 @@ extension CapturingTaskGenerationDelegate: TaskActionCreationDelegate {
         return ClangCompileTaskAction()
     }
 
+    public func createClangNonModularCompileTaskAction() -> any PlannedTaskAction {
+        return ClangNonModularCompileTaskAction()
+    }
+
     public func createClangScanTaskAction() -> any PlannedTaskAction {
         return ClangScanTaskAction()
     }
diff --git a/Tests/SWBTaskConstructionTests/DependencyVerificationTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/DependencyVerificationTaskConstructionTests.swift
index e1767d5a..fa9ed476 100644
--- a/Tests/SWBTaskConstructionTests/DependencyVerificationTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/DependencyVerificationTaskConstructionTests.swift
@@ -29,11 +29,11 @@ fileprivate struct DependencyVerificationTaskConstructionTests: CoreBasedTests {
         return "\(srcroot.str)/build/\(project).build/Debug/\(target).build/Objects-normal/x86_64/\(filename)"
     }
 
-    @Test(.requireSDKs(.macOS), .requireClangFeatures(.printHeadersDirectPerFile))
-    func addsTraceArgsWhenValidationEnabled() async throws {
+    @Test(.requireSDKs(.macOS), .requireClangFeatures(.printHeadersDirectPerFile), arguments: ["MODULE", "HEADER"])
+    func addsTraceArgsWhenValidationEnabled(depKind: String) async throws {
         try await testWith([
-            "MODULE_DEPENDENCIES": "Foo",
-            "VALIDATE_MODULE_DEPENDENCIES": "YES_ERROR"
+            "\(depKind)_DEPENDENCIES": "Foo",
+            "VALIDATE_\(depKind)_DEPENDENCIES": "YES_ERROR"
         ]) { tester, srcroot in
             await tester.checkBuild(runDestination: .macOS, fs: localFS) { results in
                 results.checkTask(.compileC(target, fileName: source)) { task in

From 067dd3a20b0520e3382e6c23220cab9c4399f97d Mon Sep 17 00:00:00 2001
From: Jake Petroules <jake.petroules@apple.com>
Date: Thu, 7 Aug 2025 17:48:48 -0700
Subject: [PATCH 44/49] Enable the static Linux SDK build

Helps keep the codebase free of unintentional Glibc-isms. There are also some interesting testing use cases enabled by building Swift Build using the static SDK.
---
 .github/workflows/pull_request.yml            |  2 ++
 Package.swift                                 |  7 ++++
 Sources/SWBUtil/Library.swift                 | 35 +++++++++++++------
 .../SWBBuildServiceConnection.swift           | 12 ++++---
 Tests/SwiftBuildTests/ServiceTests.swift      |  2 +-
 5 files changed, 41 insertions(+), 17 deletions(-)

diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
index 1f1e70d1..106b1ece 100644
--- a/.github/workflows/pull_request.yml
+++ b/.github/workflows/pull_request.yml
@@ -19,6 +19,8 @@ jobs:
       linux_swift_versions: '["nightly-main", "nightly-6.2"]'
       windows_swift_versions: '["nightly-main"]'
       windows_build_command: 'swift test --no-parallel'
+      enable_linux_static_sdk_build: true
+      linux_static_sdk_build_command: SWIFTBUILD_STATIC_LINK=1 LLBUILD_STATIC_LINK=1 swift build
   cmake-smoke-test:
     name: cmake-smoke-test
     uses: swiftlang/github-workflows/.github/workflows/swift_package_test.yml@main
diff --git a/Package.swift b/Package.swift
index f3d71a0f..99730240 100644
--- a/Package.swift
+++ b/Package.swift
@@ -20,6 +20,7 @@ let appleOS = true
 let appleOS = false
 #endif
 
+let isStaticBuild = Context.environment["SWIFTBUILD_STATIC_LINK"] != nil
 let useLocalDependencies = Context.environment["SWIFTCI_USE_LOCAL_DEPS"] != nil
 let useLLBuildFramework = Context.environment["SWIFTBUILD_LLBUILD_FWK"] != nil
 
@@ -447,6 +448,12 @@ for target in package.targets {
     }
 }
 
+if isStaticBuild {
+    package.targets = package.targets.filter { target in
+        target.type != .test && !target.name.hasSuffix("TestSupport")
+    }
+}
+
 // `SWIFTCI_USE_LOCAL_DEPS` configures if dependencies are locally available to build
 if useLocalDependencies {
     package.dependencies += [
diff --git a/Sources/SWBUtil/Library.swift b/Sources/SWBUtil/Library.swift
index a0174ad3..3412bbb1 100644
--- a/Sources/SWBUtil/Library.swift
+++ b/Sources/SWBUtil/Library.swift
@@ -63,28 +63,24 @@ public enum Library: Sendable {
         return unsafeBitCast(ptr, to: T.self)
     }
 
-    public static func locate<T>(_ pointer: T.Type) -> Path {
+    public static func locate<T>(_ pointer: T.Type) throws -> Path {
         #if os(Windows)
         var handle: HMODULE?
         guard GetModuleHandleExW(DWORD(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT), unsafeBitCast(pointer, to: LPCWSTR?.self), &handle) else {
-            return Path("")
-        }
-        do {
-            return try Path(SWB_GetModuleFileNameW(handle))
-        } catch {
-            return Path("")
+            throw SymbolLookupError(underlyingError: Win32Error(GetLastError()))
         }
+        return try Path(SWB_GetModuleFileNameW(handle))
         #else
-        let outPointer: UnsafeMutablePointer<CInterop.PlatformChar>
         var info = Dl_info()
         #if os(Android)
         dladdr(unsafeBitCast(pointer, to: UnsafeMutableRawPointer.self), &info)
-        outPointer = UnsafeMutablePointer(mutating: info.dli_fname!)
         #else
         dladdr(unsafeBitCast(pointer, to: UnsafeMutableRawPointer?.self), &info)
-        outPointer = UnsafeMutablePointer(mutating: info.dli_fname)
         #endif
-        return Path(platformString: outPointer)
+        guard let dli_fname = info.dli_fname else {
+            throw SymbolLookupError(underlyingError: nil)
+        }
+        return Path(platformString: UnsafeMutablePointer(mutating: dli_fname))
         #endif
     }
 }
@@ -102,6 +98,23 @@ public struct LibraryOpenError: Error, CustomStringConvertible, Sendable {
     }
 }
 
+public struct SymbolLookupError: Error, CustomStringConvertible, Sendable {
+    private let underlyingError: (any Error)?
+
+    public var description: String {
+        let message = "Could not locate shared object for pointer"
+        if let underlyingError {
+            return "\(message): \(underlyingError)"
+        }
+        return message
+    }
+
+    @usableFromInline
+    internal init(underlyingError: (any Error)?) {
+        self.underlyingError = underlyingError
+    }
+}
+
 // Library handles just store an opaque reference to the dlopen/LoadLibrary-returned pointer, and so are Sendable in practice based on how they are used.
 public struct LibraryHandle: @unchecked Sendable {
     #if os(Windows)
diff --git a/Sources/SwiftBuild/SWBBuildServiceConnection.swift b/Sources/SwiftBuild/SWBBuildServiceConnection.swift
index 67265583..19efb51c 100644
--- a/Sources/SwiftBuild/SWBBuildServiceConnection.swift
+++ b/Sources/SwiftBuild/SWBBuildServiceConnection.swift
@@ -63,7 +63,9 @@ typealias swb_build_service_connection_message_handler_t = @Sendable (UInt64, SW
 
     /// Absolute path to the dyld-loaded dylib binary that contains this class.
     fileprivate class var swiftbuildDylibPath: Path {
-        return Library.locate(SWBBuildServiceConnection.self)
+        get throws {
+            return try Library.locate(SWBBuildServiceConnection.self)
+        }
     }
 
     fileprivate enum State {
@@ -168,7 +170,7 @@ typealias swb_build_service_connection_message_handler_t = @Sendable (UInt64, SW
             // Compute the path to the clang ASan dylib to use when launching the ASan variant of SWBBuildService.
             // The linker adds a non-portable rpath to the directory containing the ASan dylib based on the path to the Xcode used to link the binary.  We look in Bundle.main.bundlePath (SwiftBuild_asan) for .../Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/<vers>/lib/darwin so we can relaunch with the ASan library in the default toolchain of the Xcode we're part of.
             // There are some examples of this rpath breaking which we've had to fix, e.g.: rdar://57759442&75222176
-            let asanDylib = SWBBuildServiceConnection.swiftbuildDylibPath.str.hasSuffix("_asan") ? SWBBuildServiceConnection.swiftbuildDylibPath.str : SWBBuildServiceConnection.swiftbuildDylibPath.str + "_asan"
+            let asanDylib = try SWBBuildServiceConnection.swiftbuildDylibPath.str.hasSuffix("_asan") ? SWBBuildServiceConnection.swiftbuildDylibPath.str : SWBBuildServiceConnection.swiftbuildDylibPath.str + "_asan"
             if !FileManager.default.isExecutableFile(atPath: asanDylib) {
                 // We always look for the _asan variant of the build service executable since only it will have the rpaths we need to subsequently look up. However, if it's missing we should then just fall back to the normal variant.
                 break asan
@@ -605,7 +607,7 @@ extension SWBBuildServiceVariant {
         case .default:
             // Check if the binary containing this class ends with _asan, in which case, we interpret this as a signal that we're running in asan mode, and that we should
             // load the service bundle in asan mode as well.
-            return SWBBuildServiceConnection.swiftbuildDylibPath.str.hasSuffix("_asan")
+            return (try? SWBBuildServiceConnection.swiftbuildDylibPath.str.hasSuffix("_asan")) ?? false
         case .normal:
             return false
         case .asan:
@@ -705,7 +707,7 @@ fileprivate final class InProcessStaticConnection: ConnectionTransport {
             buildServicePlugInsDirectory = URL(fileURLWithPath: path.dirname.str, isDirectory: true)
         } else {
             // If the build service executable is unbundled, then try to find the build service entry point in this executable.
-            let path = Library.locate(SWBBuildServiceConnection.self)
+            let path = try Library.locate(SWBBuildServiceConnection.self)
             // If the build service executable is unbundled, assume that any plugins that may exist are next to our executable.
             buildServicePlugInsDirectory = URL(fileURLWithPath: path.dirname.str, isDirectory: true)
         }
@@ -802,7 +804,7 @@ fileprivate final class InProcessConnection: ConnectionTransport {
             buildServicePlugInsDirectory = URL(fileURLWithPath: path.dirname.str, isDirectory: true)
         } else {
             // If the build service executable is unbundled, then try to find the build service entry point in this executable.
-            let path = Library.locate(SWBBuildServiceConnection.self)
+            let path = try Library.locate(SWBBuildServiceConnection.self)
             handle = try Library.open(path)
 
             // If the build service executable is unbundled, assume that any plugins that may exist are next to our executable.
diff --git a/Tests/SwiftBuildTests/ServiceTests.swift b/Tests/SwiftBuildTests/ServiceTests.swift
index aa928a09..f3e67770 100644
--- a/Tests/SwiftBuildTests/ServiceTests.swift
+++ b/Tests/SwiftBuildTests/ServiceTests.swift
@@ -240,7 +240,7 @@ fileprivate struct ServiceTests {
 
     @Test(.requireSDKs(.macOS), .skipSwiftPackage, .skipIfEnvironment(key: "DYLD_IMAGE_SUFFIX", value: "_asan"), .disabled(if: getEnvironmentVariable("CI")?.isEmpty == false))
     func ASanBuildService() async throws {
-        let testBundleExecutableFilePath = Library.locate(Self.self)
+        let testBundleExecutableFilePath = try Library.locate(Self.self)
 
         // Whether the current XCTest executable is running in ASan mode. Most likely never true.
         let runningWithASanSupport = testBundleExecutableFilePath.str.hasSuffix("_asan")

From bc876fe859afc62c99409b02262ef65209979a06 Mon Sep 17 00:00:00 2001
From: Mirza Garibovic <garibovic@apple.com>
Date: Thu, 7 Aug 2025 16:51:25 -0700
Subject: [PATCH 45/49] Dependencies: make xcconfig fix-its multi-line

---
 Sources/SWBCore/Dependencies.swift                        | 6 +++---
 Tests/SWBBuildSystemTests/DependencyValidationTests.swift | 8 ++++----
 2 files changed, 7 insertions(+), 7 deletions(-)

diff --git a/Sources/SWBCore/Dependencies.swift b/Sources/SWBCore/Dependencies.swift
index 5888602a..23ecfab7 100644
--- a/Sources/SWBCore/Dependencies.swift
+++ b/Sources/SWBCore/Dependencies.swift
@@ -192,14 +192,14 @@ public struct ModuleDependenciesContext: Sendable, SerializableCodable {
         }
 
         func makeFixIt(newModules: [ModuleDependency]) -> Diagnostic.FixIt {
-            let stringValue = newModules.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().joined(separator: " ")
+            let stringValue = newModules.map { $0.asBuildSettingEntryQuotedIfNeeded }.sorted().map { " \\\n    \($0)" }.joined(separator: "")
             let newText: String
             switch modificationStyle {
             case .appendToExistingAssignment:
-                newText = " \(stringValue)"
+                newText = stringValue
             case .insertNewAssignment(let targetNameCondition):
                 let targetCondition = targetNameCondition.map { "[target=\($0)]" } ?? ""
-                newText = "\n\(BuiltinMacros.MODULE_DEPENDENCIES.name)\(targetCondition) = $(inherited) \(stringValue)\n"
+                newText = "\n\(BuiltinMacros.MODULE_DEPENDENCIES.name)\(targetCondition) = $(inherited)\(stringValue)\n"
             }
 
             return Diagnostic.FixIt(sourceRange: sourceRange, newText: newText)
diff --git a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
index 9a5955e9..cfb51a93 100644
--- a/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
+++ b/Tests/SWBBuildSystemTests/DependencyValidationTests.swift
@@ -421,7 +421,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                         fixIts: [
                             Diagnostic.FixIt(
                                 sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 47, endLine: 1, endColumn: 47),
-                                newText: " Foundation"),
+                                newText: " \\\n    Foundation"),
                         ],
                         childDiagnostics: [
                             Diagnostic(
@@ -430,7 +430,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                                 data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"),
                                 fixIts: [Diagnostic.FixIt(
                                     sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: 1, startColumn: 47, endLine: 1, endColumn: 47),
-                                    newText: " Foundation")],
+                                    newText: " \\\n    Foundation")],
                             ),
                         ]),
                 ],
@@ -442,7 +442,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                         fixIts: [
                             Diagnostic.FixIt(
                                 sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max),
-                                newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n"),
+                                newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) \\\n    Foundation\n"),
                         ],
                         childDiagnostics: [
                             Diagnostic(
@@ -451,7 +451,7 @@ fileprivate struct DependencyValidationTests: CoreBasedTests {
                                 data: DiagnosticData("Missing entry in MODULE_DEPENDENCIES: Foundation"),
                                 fixIts: [Diagnostic.FixIt(
                                     sourceRange: Diagnostic.SourceRange(path: projectXCConfigPath, startLine: .max, startColumn: .max, endLine: .max, endColumn: .max),
-                                    newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) Foundation\n")],
+                                    newText: "\nMODULE_DEPENDENCIES[target=TargetB] = $(inherited) \\\n    Foundation\n")],
                             ),
                         ]),
                 ],

From a2443f74e1926b953908544aaaffdcafa05d5375 Mon Sep 17 00:00:00 2001
From: Michael Rawdon <rawdon@apple.com>
Date: Thu, 7 Aug 2025 17:56:06 -0700
Subject: [PATCH 46/49] Fix typo in description of GCC_SHORT_ENUMS build
 setting

rdar://154675013
---
 Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings b/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings
index bd8b3100..cd3b6451 100644
--- a/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings	
+++ b/Sources/SWBUniversalPlatform/Specs/en.lproj/Apple Clang.strings	
@@ -207,7 +207,7 @@ Pascal string literals take the form `\"\\pstring\"` . The special escape sequen
 "[GCC_SHORT_ENUMS]-name" = "Short Enumeration Constants";
 "[GCC_SHORT_ENUMS]-description" = "Make enums only as large as needed for the range of possible values.
 
-This setting generates code that may not binary compatible with code generated without this setting or with macOS frameworks.";
+This setting generates code that may not be binary compatible with code generated without this setting or with macOS frameworks.";
 "[GCC_SHORT_ENUMS]-value-[NO]" = "No";
 "[GCC_SHORT_ENUMS]-value-[YES]" = "Yes";
 

From d1a00c21bddf75d883b085ddcd8614916df06528 Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu@apple.com>
Date: Mon, 11 Aug 2025 09:34:43 -0700
Subject: [PATCH 47/49] [Swift] Add crash reproducer support (#686)

Add support for swift crash reproducer so a crash reproducer is created
if supported when a swift compiler crash happened.
---
 .../SWBCore/LibSwiftDriver/PlannedBuild.swift | 10 +++++++++
 .../SwiftDriverJobTaskAction.swift            | 21 +++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/Sources/SWBCore/LibSwiftDriver/PlannedBuild.swift b/Sources/SWBCore/LibSwiftDriver/PlannedBuild.swift
index 5a3332b0..5b7a5685 100644
--- a/Sources/SWBCore/LibSwiftDriver/PlannedBuild.swift
+++ b/Sources/SWBCore/LibSwiftDriver/PlannedBuild.swift
@@ -529,6 +529,16 @@ extension LibSwiftDriver {
             }
         }
 
+        public func getCrashReproducerCommand(for job: PlannedSwiftDriverJob, output dir: Path) async throws -> [String]? {
+            try await dispatchQueue.sync {
+                let driverJob = try self.driverJob(for: job)
+                guard let reproJob = self.jobExecutionDelegate?.getReproducerJob(job: driverJob, output: try VirtualPath(path: dir.str)) else {
+                  return nil
+                }
+                return try self.argsResolver.resolveArgumentList(for: reproJob, useResponseFiles: .heuristic)
+            }
+        }
+
         public func getDiscoveredJobsAfterFinishing(job: PlannedSwiftDriverJob) throws -> [PlannedSwiftDriverJob] {
 
             dispatchQueue.blocking_sync {
diff --git a/Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift b/Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift
index 7851c42d..2ed927e5 100644
--- a/Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift
+++ b/Sources/SWBTaskExecution/TaskActions/SwiftDriverJobTaskAction.swift
@@ -420,6 +420,7 @@ public final class SwiftDriverJobTaskAction: TaskAction, BuildValueValidatingTas
                 private var pid = llbuild_pid_t.invalid
 
                 var executionError: String?
+                var wasSignaled: Bool = false
                 private var processStarted = false
                 private var _commandResult: CommandResult?
                 var commandResult: CommandResult? {
@@ -469,10 +470,15 @@ public final class SwiftDriverJobTaskAction: TaskAction, BuildValueValidatingTas
                 }
 
                 func processFinished(result: CommandExtendedResult) {
+                    if wasSignaled {
+                        // If the process was already signaled, this might be in a reproducer creation. No need to update finish status.
+                        return
+                    }
                     guard let status = Processes.ExitStatus.init(rawValue: result.exitStatus) else {
                         // nil means the job is stopped or continued. It should not call finished.
                         return
                     }
+                    wasSignaled = status.wasSignaled
                     // This may be updated by commandStarted in the case of certain failures,
                     // so only update the exit status in output delegate if it is nil.
                     if outputDelegate.result == nil {
@@ -526,6 +532,21 @@ public final class SwiftDriverJobTaskAction: TaskAction, BuildValueValidatingTas
 
             try await spawn(commandLine: options.commandLine, environment: environment, workingDirectory: task.workingDirectory, dynamicExecutionDelegate: dynamicExecutionDelegate, clientDelegate: clientDelegate, processDelegate: delegate)
 
+            // Generate crash reproducoer.
+            if delegate.wasSignaled {
+                // The output directory for crash reproducer is:
+                // * Specified by environment
+                // * Primary output path directory
+                // * Temp directory
+                let reproDir = environment["SWIFT_CRASH_DIAGNOSTICS_DIR"].map(Path.init) ?? driverJob.driverJob.outputs.first?.dirname
+                try await withTemporaryDirectory(dir: reproDir, prefix: "swift-crash-reproducer", removeTreeOnDeinit: false) { dir in
+                    if let reproCommand = try await plannedBuild?.getCrashReproducerCommand(for: driverJob, output: dir) {
+                        try await spawn(commandLine: reproCommand, environment: environment, workingDirectory: task.workingDirectory, dynamicExecutionDelegate: dynamicExecutionDelegate, clientDelegate: clientDelegate, processDelegate: delegate)
+                        outputDelegate.note("Crash reproducer created in \(dir.str)")
+                    }
+                }
+            }
+
             if let error = delegate.executionError {
                 outputDelegate.error(error)
                 return .failed

From 22c21180272eaa110a9f56c3e93917dc59d402f4 Mon Sep 17 00:00:00 2001
From: Michael Rawdon <rawdon@apple.com>
Date: Mon, 11 Aug 2025 15:12:22 -0700
Subject: [PATCH 48/49] Adjusted expected number of CreateBuildDirectory tasks
 in
 TaskConstructionPerfTests.createBuildDirectoryTaskGenerationWith500Targets()
 now that explicit module directories are being created.

rdar://157621853
---
 .../TaskConstructionPerfTests.swift                             | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Tests/SWBTaskConstructionPerfTests/TaskConstructionPerfTests.swift b/Tests/SWBTaskConstructionPerfTests/TaskConstructionPerfTests.swift
index 9971e2b6..d9ffa8b7 100644
--- a/Tests/SWBTaskConstructionPerfTests/TaskConstructionPerfTests.swift
+++ b/Tests/SWBTaskConstructionPerfTests/TaskConstructionPerfTests.swift
@@ -54,7 +54,7 @@ fileprivate struct TaskConstructionPerfTests: CoreBasedTests, PerfTests {
             await measure {
                 await tester.checkBuild(runDestination: .macOS, checkTaskGraphIntegrity: false) { tester in
                     tester.checkTasks(.matchRuleType("CreateBuildDirectory")) { tasks in
-                        #expect(tasks.count == 1503)
+                        #expect(tasks.count == 2505)
                     }
                 }
             }

From 5d14073466973ca9988b83d25f3bb5a7f20b9ce1 Mon Sep 17 00:00:00 2001
From: Owen Voorhees <ovoorhees@apple.com>
Date: Tue, 12 Aug 2025 10:46:38 -0700
Subject: [PATCH 49/49] Allow opting into Swift Testing entrypoints for macOS
 test bundles when building with the SwiftPM CLI

---
 Sources/SWBCore/Settings/BuiltinMacros.swift  |  2 +
 .../Specs/ProductTypes.xcspec                 |  4 +
 .../TestEntryPointGenerationTaskAction.swift  | 96 ++++++++++---------
 .../TestEntryPointGenerationTool.swift        | 24 +++--
 .../BuildOperationTests.swift                 | 70 ++++++++++++++
 .../UnitTestTaskConstructionTests.swift       |  2 +-
 6 files changed, 145 insertions(+), 53 deletions(-)

diff --git a/Sources/SWBCore/Settings/BuiltinMacros.swift b/Sources/SWBCore/Settings/BuiltinMacros.swift
index d0f5dbd5..2c5448b6 100644
--- a/Sources/SWBCore/Settings/BuiltinMacros.swift
+++ b/Sources/SWBCore/Settings/BuiltinMacros.swift
@@ -726,6 +726,7 @@ public final class BuiltinMacros {
     public static let GENERATE_RESOURCE_ACCESSORS = BuiltinMacros.declareBooleanMacro("GENERATE_RESOURCE_ACCESSORS")
     public static let GENERATE_TEST_ENTRY_POINT = BuiltinMacros.declareBooleanMacro("GENERATE_TEST_ENTRY_POINT")
     public static let GENERATED_TEST_ENTRY_POINT_PATH = BuiltinMacros.declarePathMacro("GENERATED_TEST_ENTRY_POINT_PATH")
+    public static let GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS = BuiltinMacros.declareBooleanMacro("GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS")
     public static let GENERATE_TEXT_BASED_STUBS = BuiltinMacros.declareBooleanMacro("GENERATE_TEXT_BASED_STUBS")
     public static let GENERATE_INTERMEDIATE_TEXT_BASED_STUBS = BuiltinMacros.declareBooleanMacro("GENERATE_INTERMEDIATE_TEXT_BASED_STUBS")
     public static let GLOBAL_API_NOTES_PATH = BuiltinMacros.declareStringMacro("GLOBAL_API_NOTES_PATH")
@@ -1792,6 +1793,7 @@ public final class BuiltinMacros {
         GENERATE_RESOURCE_ACCESSORS,
         GENERATE_TEST_ENTRY_POINT,
         GENERATED_TEST_ENTRY_POINT_PATH,
+        GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS,
         GENERATE_TEXT_BASED_STUBS,
         GENERATE_INTERMEDIATE_TEXT_BASED_STUBS,
         GID,
diff --git a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
index e8211ed1..647cdaea 100644
--- a/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
+++ b/Sources/SWBUniversalPlatform/Specs/ProductTypes.xcspec
@@ -335,6 +335,9 @@
 
             PROVISIONING_PROFILE_SUPPORTED = YES;
             PROVISIONING_PROFILE_REQUIRED = NO;
+
+            GENERATE_TEST_ENTRY_POINT = "$(GENERATE_TEST_ENTRYPOINTS_FOR_BUNDLES)";
+            GENERATED_TEST_ENTRY_POINT_PATH = "$(DERIVED_SOURCES_DIR)/test_entry_point.swift";
         };
         PackageTypes = (
             com.apple.package-type.bundle.unit-test
@@ -353,6 +356,7 @@
             ENABLE_TESTING_SEARCH_PATHS = YES;
             GENERATE_TEST_ENTRY_POINT = YES;
             GENERATED_TEST_ENTRY_POINT_PATH = "$(DERIVED_SOURCES_DIR)/test_entry_point.swift";
+            GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS = YES;
         };
         PackageTypes = (
             com.apple.package-type.mach-o-executable
diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift
index 9765719a..db59e7ac 100644
--- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift
+++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTaskAction.swift
@@ -25,24 +25,30 @@ class TestEntryPointGenerationTaskAction: TaskAction {
             let options = try Options.parse(Array(task.commandLineAsStrings.dropFirst()))
 
             var tests: [IndexStore.TestCaseClass] = []
-            var objects: [Path] = []
-            for linkerFilelist in options.linkerFilelist {
-                let filelistContents = String(String(decoding: try executionDelegate.fs.read(linkerFilelist), as: UTF8.self))
-                let entries = filelistContents.split(separator: "\n", omittingEmptySubsequences: true).map { Path($0) }.map {
-                    for indexUnitBasePath in options.indexUnitBasePath {
-                        if let remappedPath = generateIndexOutputPath(from: $0, basePath: indexUnitBasePath) {
-                            return remappedPath
+            if options.discoverTests {
+                var objects: [Path] = []
+                for linkerFilelist in options.linkerFilelist {
+                    let filelistContents = String(String(decoding: try executionDelegate.fs.read(linkerFilelist), as: UTF8.self))
+                    let entries = filelistContents.split(separator: "\n", omittingEmptySubsequences: true).map { Path($0) }.map {
+                        for indexUnitBasePath in options.indexUnitBasePath {
+                            if let remappedPath = generateIndexOutputPath(from: $0, basePath: indexUnitBasePath) {
+                                return remappedPath
+                            }
                         }
+                        return $0
                     }
-                    return $0
+                    objects.append(contentsOf: entries)
+                }
+                guard let indexStoreLibraryPath = options.indexStoreLibraryPath else {
+                    outputDelegate.emitError("Test discovery was requested, but failed to lookup index store library in toolchain")
+                    return .failed
+                }
+                let indexStoreAPI = try IndexStoreAPI(dylib: indexStoreLibraryPath)
+                for indexStore in options.indexStore {
+                    let store = try IndexStore.open(store: indexStore, api: indexStoreAPI)
+                    let testInfo = try store.listTests(in: objects)
+                    tests.append(contentsOf: testInfo)
                 }
-                objects.append(contentsOf: entries)
-            }
-            let indexStoreAPI = try IndexStoreAPI(dylib: options.indexStoreLibraryPath)
-            for indexStore in options.indexStore {
-                let store = try IndexStore.open(store: indexStore, api: indexStoreAPI)
-                let testInfo = try store.listTests(in: objects)
-                tests.append(contentsOf: testInfo)
             }
 
             try executionDelegate.fs.write(options.output, contents: ByteString(encodingAsUTF8: """
@@ -52,7 +58,7 @@ class TestEntryPointGenerationTaskAction: TaskAction {
             
             \(testObservationFragment)
             
-            import XCTest
+            public import XCTest
             \(discoveredTestsFragment(tests: tests))
 
             @main
@@ -94,16 +100,7 @@ class TestEntryPointGenerationTaskAction: TaskAction {
                         }
                     }
                     #endif
-                    if testingLibrary == "xctest" {
-                        #if !os(Windows) && \(options.enableExperimentalTestOutput)
-                        _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) }
-                        #endif
-                        #if os(WASI)
-                        await XCTMain(__allDiscoveredTests()) as Never
-                        #else
-                        XCTMain(__allDiscoveredTests()) as Never
-                        #endif
-                    }
+                    \(xctestFragment(enableExperimentalTestOutput: options.enableExperimentalTestOutput, disable: !options.discoverTests))
                 }
                 #else
                 static func main() async {
@@ -113,16 +110,7 @@ class TestEntryPointGenerationTaskAction: TaskAction {
                         await Testing.__swiftPMEntryPoint() as Never
                     }
                     #endif
-                    if testingLibrary == "xctest" {
-                        #if !os(Windows) && \(options.enableExperimentalTestOutput)
-                        _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) }
-                        #endif
-                        #if os(WASI)
-                        await XCTMain(__allDiscoveredTests()) as Never
-                        #else
-                        XCTMain(__allDiscoveredTests()) as Never
-                        #endif
-                    }
+                    \(xctestFragment(enableExperimentalTestOutput: options.enableExperimentalTestOutput, disable: !options.discoverTests))
                 }
                 #endif
             }
@@ -137,14 +125,18 @@ class TestEntryPointGenerationTaskAction: TaskAction {
 
     private struct Options: ParsableArguments {
         @Option var output: Path
-        @Option var indexStoreLibraryPath: Path
-        @Option var linkerFilelist: [Path]
-        @Option var indexStore: [Path]
-        @Option var indexUnitBasePath: [Path]
+        @Option var indexStoreLibraryPath: Path? = nil
+        @Option() var linkerFilelist: [Path] = []
+        @Option var indexStore: [Path] = []
+        @Option var indexUnitBasePath: [Path] = []
         @Flag var enableExperimentalTestOutput: Bool = false
+        @Flag var discoverTests: Bool = false
     }
 
     private func discoveredTestsFragment(tests: [IndexStore.TestCaseClass]) -> String {
+        guard !tests.isEmpty else {
+            return ""
+        }
         var fragment = ""
         for moduleName in Set(tests.map { $0.module }).sorted() {
             fragment += "@testable import \(moduleName)\n"
@@ -174,11 +166,29 @@ class TestEntryPointGenerationTaskAction: TaskAction {
         return fragment
     }
 
+    private func xctestFragment(enableExperimentalTestOutput: Bool, disable: Bool) -> String {
+        guard !disable else {
+            return ""
+        }
+        return """
+        if testingLibrary == "xctest" {
+            #if !os(Windows) && \(enableExperimentalTestOutput)
+            _ = Self.testOutputPath().map { SwiftPMXCTestObserver(testOutputPath: testOutputPath) }
+            #endif
+            #if os(WASI)
+            await XCTMain(__allDiscoveredTests()) as Never
+            #else
+            XCTMain(__allDiscoveredTests()) as Never
+            #endif
+        }
+        """
+    }
+
     private var testObservationFragment: String =
         """
         #if !os(Windows) // Test observation is not supported on Windows
-        import Foundation
-        import XCTest
+        public import Foundation
+        public import XCTest
         
         public final class SwiftPMXCTestObserver: NSObject {
             let testOutputPath: String
@@ -562,7 +572,7 @@ class TestEntryPointGenerationTaskAction: TaskAction {
             }
         }
         
-        import XCTest
+        public import XCTest
         
         #if canImport(Darwin) // XCTAttachment is unavailable in swift-corelibs-xctest.
         extension TestAttachment {
diff --git a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift
index e8e97f25..ec9dbfb7 100644
--- a/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift
+++ b/Sources/SWBUniversalPlatform/TestEntryPointGenerationTool.swift
@@ -19,9 +19,13 @@ final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecId
 
     override func commandLineFromTemplate(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, optionContext: (any DiscoveredCommandLineToolSpecInfo)?, specialArgs: [String] = [], lookup: ((MacroDeclaration) -> MacroExpression?)? = nil) async -> [CommandLineArgument] {
         var args = await super.commandLineFromTemplate(cbc, delegate, optionContext: optionContext, specialArgs: specialArgs, lookup: lookup)
-        for (toolchainPath, toolchainLibrarySearchPath) in cbc.producer.toolchains.map({ ($0.path, $0.librarySearchPaths) }) {
-            if let path = toolchainLibrarySearchPath.findLibrary(operatingSystem: cbc.producer.hostOperatingSystem, basename: "IndexStore") {
-                args.append(contentsOf: ["--index-store-library-path", .path(path)])
+        if cbc.scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS) {
+            args.append("--discover-tests")
+            for toolchainLibrarySearchPath in cbc.producer.toolchains.map({ $0.librarySearchPaths }) {
+                if let path = toolchainLibrarySearchPath.findLibrary(operatingSystem: cbc.producer.hostOperatingSystem, basename: "IndexStore") {
+                    args.append(contentsOf: ["--index-store-library-path", .path(path)])
+                    break
+                }
             }
             for input in cbc.inputs {
                 if input.fileType.conformsTo(identifier: "text") {
@@ -43,12 +47,14 @@ final class TestEntryPointGenerationToolSpec: GenericCommandLineToolSpec, SpecId
     public func constructTasks(_ cbc: CommandBuildContext, _ delegate: any TaskGenerationDelegate, indexStorePaths: [Path], indexUnitBasePaths: [Path]) async {
         var commandLine = await commandLineFromTemplate(cbc, delegate, optionContext: nil)
 
-        for indexStorePath in indexStorePaths {
-            commandLine.append(contentsOf: ["--index-store", .path(indexStorePath)])
-        }
-
-        for basePath in indexUnitBasePaths {
-            commandLine.append(contentsOf: ["--index-unit-base-path", .path(basePath)])
+        if cbc.scope.evaluate(BuiltinMacros.GENERATED_TEST_ENTRY_POINT_INCLUDE_DISCOVERED_TESTS) {
+            for indexStorePath in indexStorePaths {
+                commandLine.append(contentsOf: ["--index-store", .path(indexStorePath)])
+            }
+            
+            for basePath in indexUnitBasePaths {
+                commandLine.append(contentsOf: ["--index-unit-base-path", .path(basePath)])
+            }
         }
 
         delegate.createTask(
diff --git a/Tests/SWBBuildSystemTests/BuildOperationTests.swift b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
index cf9ebad8..8bac3097 100644
--- a/Tests/SWBBuildSystemTests/BuildOperationTests.swift
+++ b/Tests/SWBBuildSystemTests/BuildOperationTests.swift
@@ -382,6 +382,76 @@ fileprivate struct BuildOperationTests: CoreBasedTests {
         }
     }
 
+    @Test(.requireSDKs(.macOS))
+    func unitTestWithGeneratedEntryPointViaMacOSOverride() async throws {
+        try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in
+            let testProject = try await TestProject(
+                "TestProject",
+                sourceRoot: tmpDir,
+                groupTree: TestGroup(
+                    "SomeFiles",
+                    children: [
+                        TestFile("test.swift"),
+                    ]),
+                buildConfigurations: [
+                    TestBuildConfiguration("Debug", buildSettings: [
+                        "ARCHS": "$(ARCHS_STANDARD)",
+                        "CODE_SIGNING_ALLOWED": "NO",
+                        "PRODUCT_NAME": "$(TARGET_NAME)",
+                        "SDKROOT": "$(HOST_PLATFORM)",
+                        "SUPPORTED_PLATFORMS": "$(HOST_PLATFORM)",
+                        "SWIFT_VERSION": swiftVersion,
+                        "INDEX_DATA_STORE_DIR": "\(tmpDir.join("index").str)",
+                        "LINKER_DRIVER": "swiftc"
+                    ])
+                ],
+                targets: [
+                    TestStandardTarget(
+                        "MyTests",
+                        type: .unitTest,
+                        buildConfigurations: [
+                            TestBuildConfiguration("Debug", buildSettings: [
+                                "GENERATE_TEST_ENTRYPOINTS_FOR_BUNDLES": "YES"
+                            ])
+                        ],
+                        buildPhases: [
+                            TestSourcesBuildPhase(["test.swift"]),
+                        ],
+                    ),
+                ])
+            let core = try await getCore()
+            let tester = try await BuildOperationTester(core, testProject, simulated: false)
+            try localFS.createDirectory(tmpDir.join("index"))
+            let projectDir = tester.workspace.projects[0].sourceRoot
+
+            try await tester.fs.writeFileContents(projectDir.join("test.swift")) { stream in
+                stream <<< """
+                    import Testing
+                    import XCTest
+                    @Suite struct MySuite {
+                        @Test func myTest() {
+                            #expect(42 == 42)
+                        }
+                    }
+
+                    final class MYXCTests: XCTestCase {
+                        func testFoo() {
+                            XCTAssertTrue(true)
+                        }
+                    }
+                """
+            }
+
+            let destination: RunDestinationInfo = .host
+            try await tester.checkBuild(runDestination: destination, persistent: true) { results in
+                results.checkNoErrors()
+                results.checkTask(.matchRuleType("GenerateTestEntryPoint")) { task in
+                    task.checkCommandLineMatches(["builtin-generateTestEntryPoint", "--output", .suffix("test_entry_point.swift")])
+                }
+            }
+        }
+    }
+
     @Test(.requireSDKs(.host), .skipHostOS(.macOS), .skipHostOS(.windows, "cannot find testing library"))
     func unitTestWithGeneratedEntryPoint() async throws {
         try await withTemporaryDirectory(removeTreeOnDeinit: false) { (tmpDir: Path) in
diff --git a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift
index 36c60d39..cd27640e 100644
--- a/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift
+++ b/Tests/SWBTaskConstructionTests/UnitTestTaskConstructionTests.swift
@@ -366,7 +366,7 @@ fileprivate struct UnitTestTaskConstructionTests: CoreBasedTests {
         await tester.checkBuild(runDestination: .linux, fs: fs) { results in
             results.checkTarget("UnitTestRunner") { target in
                 results.checkTask(.matchTarget(target), .matchRuleType("GenerateTestEntryPoint")) { task in
-                    task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"])
+                    task.checkCommandLineMatches([.suffix("builtin-generateTestEntryPoint"), "--output", .suffix("test_entry_point.swift"), "--discover-tests", "--index-store-library-path", .suffix("libIndexStore.so"), "--linker-filelist", .suffix("UnitTestTarget.LinkFileList"), "--index-store", "/index", "--index-unit-base-path", "/tmp/Test/aProject/build"])
                     task.checkInputs([
                         .pathPattern(.suffix("UnitTestTarget.LinkFileList")),
                         .pathPattern(.suffix("UnitTestTarget.so")),
