Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Sources/SWBApplePlatform/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ add_library(SWBApplePlatform
AssetCatalogCompilerOutputParser.swift
CMakeLists.txt
DevelopmentAssetsTaskProducer.swift
EntityLinkerTool.swift
ImageScaleFactorsInputFileGroupingStrategy.swift
InterfaceBuilderCompilerOutputParser.swift
LocalizationInputFileGroupingStrategy.swift
Expand All @@ -46,6 +47,7 @@ add_library(SWBApplePlatform
PluginDCLT.swift
RealityAssetsTaskProducer.swift
RegisterExecutionPolicyException.swift
SsafAnalyzerTool.swift
StringCatalogCompilerOutputParser.swift
StubBinaryTaskProducer.swift
XCStringsInputFileGroupingStrategy.swift)
Expand All @@ -70,6 +72,7 @@ SwiftBuild_Bundle(MODULE SWBApplePlatform FILES
Specs/Embedded-Simulator.xcspec
Specs/EmbeddedBinaryValidationUtility.xcspec
Specs/EXUtil.xcspec
Specs/EntityLinker.xcspec
Specs/GenerateAppPlaygroundAssetCatalog.xcspec
Specs/GenerateTextureAtlas.xcspec
Specs/IBCompiler.xcspec
Expand Down Expand Up @@ -110,6 +113,7 @@ SwiftBuild_Bundle(MODULE SWBApplePlatform FILES
Specs/SceneKitFileTypes.xcspec
Specs/SceneKitTools.xcspec
Specs/SpriteKitFileTypes.xcspec
Specs/SsafAnalyzer.xcspec
Specs/StripSymbolsDarwin.xcspec
Specs/TiffUtil.xcspec
Specs/tvOSDevice.xcspec
Expand Down
10 changes: 1 addition & 9 deletions Sources/SWBApplePlatform/EntityLinkerTool.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ public final class EntityLinkerToolSpec: GenericCommandLineToolSpec, SpecIdentif

override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? {
let toolPath = self.resolveExecutablePath(producer, Path("clang-ssaf-linker"))

do {
return try await DiscoveredEntityLinkerToolSpecInfo.parseProjectNameAndSourceVersionStyleVersionInfo(producer, delegate, commandLine: [toolPath.str, "version"]) { versionInfo in
DiscoveredEntityLinkerToolSpecInfo(toolPath: toolPath, toolVersion: versionInfo.version)
}
} catch {
delegate.error(error)
return nil
}
return DiscoveredEntityLinkerToolSpecInfo(toolPath: toolPath, toolVersion: nil)
}
}
2 changes: 2 additions & 0 deletions Sources/SWBApplePlatform/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ struct ApplePlatformSpecsExtension: SpecificationsExtension {
[
AppExtensionPlistGeneratorSpec.self,
AppIntentsMetadataCompilerSpec.self,
EntityLinkerToolSpec.self,
SsafAnalyzerToolSpec.self,
AppIntentsSSUTrainingCompilerSpec.self,
ExtensionPointExtractorSpec.self,
ActoolCompilerSpec.self,
Expand Down
30 changes: 30 additions & 0 deletions Sources/SWBApplePlatform/Specs/SsafAnalyzer.xcspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2026 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.build-tools.clang-ssaf-analyzer;
ExecPath = "clang-ssaf-analyzer";
Name = "SSAF Analyzer Tool";
Description = "Analyzes linked entity summary files";
CommandLine = "clang-ssaf-analyzer [special-args] [inputs] -o $(OutputPath)";
RuleName = "AnalyzeSSAF $(OutputPath)";
ExecDescription = "Analyzing Summary files";
ProgressDescription = "Analyzing $(OutputPath)";
InputFileTypes = ();
CommandOutputParser = XCGenericCommandOutputParser;
InputFileGroupings = ();
SynthesizeBuildRule = YES;
IsArchitectureNeutral = NO;
}
)
34 changes: 34 additions & 0 deletions Sources/SWBApplePlatform/SsafAnalyzerTool.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2026 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 SWBUtil
public import SWBCore
public import SWBMacro


public final class SsafAnalyzerToolSpec: GenericCommandLineToolSpec, SpecIdentifierType, @unchecked Sendable {
public static let identifier = "com.apple.build-tools.clang-ssaf-analyzer"
required public init(_ parser: SpecParser, _ basedOnSpec: Spec?) {
super.init(parser, basedOnSpec)
}

public struct DiscoveredSsafAnalyzerToolSpecInfo: DiscoveredCommandLineToolSpecInfo {
public let toolPath: Path
public var toolVersion: Version?
}

override public func discoveredCommandLineToolSpecInfo(_ producer: any CommandProducer, _ scope: MacroEvaluationScope, _ delegate: any CoreClientTargetDiagnosticProducingDelegate) async -> (any DiscoveredCommandLineToolSpecInfo)? {
let toolPath = self.resolveExecutablePath(producer, Path("clang-ssaf-analyzer"))
return DiscoveredSsafAnalyzerToolSpecInfo(toolPath: toolPath, toolVersion: nil)
}
}
2 changes: 2 additions & 0 deletions Sources/SWBCore/Settings/BuiltinMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ public final class BuiltinMacros {
public static let RUN_CLANG_STATIC_ANALYZER = BuiltinMacros.declareBooleanMacro("RUN_CLANG_STATIC_ANALYZER")
public static let INVOKE_SSAF = BuiltinMacros.declareBooleanMacro("INVOKE_SSAF")
public static let EXTRACT_SUMMARIES = BuiltinMacros.declareStringMacro("EXTRACT_SUMMARIES")
public static let STOP_AT_LU_SUMMARY_GENERATION = BuiltinMacros.declareStringListMacro("STOP_AT_LU_SUMMARY_GENERATION")
public static let SWIFT_API_DIGESTER_MODE = BuiltinMacros.declareEnumMacro("SWIFT_API_DIGESTER_MODE") as EnumMacroDeclaration<SwiftAPIDigesterMode>
public static let RUN_SWIFT_ABI_CHECKER_TOOL = BuiltinMacros.declareBooleanMacro("RUN_SWIFT_ABI_CHECKER_TOOL")
public static let RUN_SWIFT_ABI_CHECKER_TOOL_DRIVER = BuiltinMacros.declareBooleanMacro("RUN_SWIFT_ABI_CHECKER_TOOL_DRIVER")
Expand Down Expand Up @@ -2242,6 +2243,7 @@ public final class BuiltinMacros {
RUN_CLANG_STATIC_ANALYZER,
INVOKE_SSAF,
EXTRACT_SUMMARIES,
STOP_AT_LU_SUMMARY_GENERATION,
RUN_DOCUMENTATION_COMPILER,
SKIP_BUILDING_DOCUMENTATION,
RUN_SYMBOL_GRAPH_EXTRACT,
Expand Down
1 change: 1 addition & 0 deletions Sources/SWBCore/TaskGeneration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ public protocol CommandProducer: PlatformBuildContext, SpecLookupContext, Refere
var clangStaticAnalyzerSpec: ClangCompilerSpec { get }

var entityLinkerToolSpec: CommandLineToolSpec { get }
var ssafAnalyzerToolSpec: CommandLineToolSpec { get }

/// The Clang modules verifier tool spec to use.
var clangModuleVerifierSpec: ClangCompilerSpec { get }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1004,6 +1004,16 @@ package final class SourcesTaskProducer: FilesBasedBuildPhaseTaskProducerBase, F
let output = Path(binaryOutput.str + ".linked-summaries.json")
await context.entityLinkerToolSpec.constructTasks(CommandBuildContext(producer: context, scope: scope, inputs: ssafInputs, output: output), delegate)
}
await appendGeneratedTasks(&perArchTasks) { delegate in
let linkedSummariesInput = Path(binaryOutput.str + ".linked-summaries.json")
let analyzerOutput = Path(binaryOutput.str + ".ssaf-analysis.json")
let analysisName = scope.evaluate(BuiltinMacros.EXTRACT_SUMMARIES)
let stopAtAnalyses = Set(scope.evaluate(BuiltinMacros.STOP_AT_LU_SUMMARY_GENERATION))
let specialArgs = analysisName.split(separator: ",")
.filter { !stopAtAnalyses.contains(String($0)) }
.flatMap { ["-a", "\($0)AnalysisResult"] }
await context.ssafAnalyzerToolSpec.constructTasks(CommandBuildContext(producer: context, scope: scope, inputs: [FileToBuild(context: context, absolutePath: linkedSummariesInput)], output: analyzerOutput), delegate, specialArgs: specialArgs)
}
}

// Handle linking prelinked objects. Presently we always do this if GENERATE_PRELINK_OBJECT_FILE even if there are no other tasks, since PRELINK_LIBS or PRELINK_FLAGS might be set to values which will cause a prelinked object file to be generated.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
public let clangPreprocessorSpec: ClangCompilerSpec
public let clangStaticAnalyzerSpec: ClangCompilerSpec
public let entityLinkerToolSpec: CommandLineToolSpec
public let ssafAnalyzerToolSpec: CommandLineToolSpec
public let clangModuleVerifierSpec: ClangCompilerSpec
private let _clangStatCacheSpec: Result<ClangStatCacheSpec, any Error>
var clangStatCacheSpec: ClangStatCacheSpec? { return specForResult(_clangStatCacheSpec) }
Expand Down Expand Up @@ -358,6 +359,7 @@ public class TaskProducerContext: StaleFileRemovalContext, BuildFileResolution
self.clangPreprocessorSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangPreprocessorSpec.self)
self.clangStaticAnalyzerSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangStaticAnalyzerSpec.self)
self.entityLinkerToolSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.clang-ssaf-linker", domain: domain, ofType: CommandLineToolSpec.self)
self.ssafAnalyzerToolSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.clang-ssaf-analyzer", domain: domain, ofType: CommandLineToolSpec.self)
self.clangModuleVerifierSpec = try! workspaceContext.core.specRegistry.getSpec(domain: domain, ofType: ClangModuleVerifierSpec.self)
self._clangStatCacheSpec = Result { try workspaceContext.core.specRegistry.getSpec("com.apple.compilers.clang-stat-cache", ofType: ClangStatCacheSpec.self) }
self.codesignSpec = try! workspaceContext.core.specRegistry.getSpec("com.apple.build-tools.codesign", domain: domain, ofType: CodesignToolSpec.self)
Expand Down
2 changes: 2 additions & 0 deletions Sources/SWBTestSupport/DummyCommandProducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ package struct MockCommandProducer: CommandProducer, Sendable {
self.clangPreprocessorSpec = try getSpec(ofType: ClangPreprocessorSpec.self)
self.clangStaticAnalyzerSpec = try getSpec(ofType: ClangStaticAnalyzerSpec.self)
self.entityLinkerToolSpec = try getSpec("com.apple.build-tools.clang-ssaf-linker", ofType: CommandLineToolSpec.self)
self.ssafAnalyzerToolSpec = try getSpec("com.apple.build-tools.clang-ssaf-analyzer", ofType: CommandLineToolSpec.self)
self.clangModuleVerifierSpec = try getSpec(ofType: ClangModuleVerifierSpec.self)
self.diffSpec = try getSpec("com.apple.build-tools.diff", ofType: CommandLineToolSpec.self)
self.stripSpec = try getSpec("com.apple.build-tools.strip", ofType: StripToolSpec.self)
Expand Down Expand Up @@ -165,6 +166,7 @@ package struct MockCommandProducer: CommandProducer, Sendable {
package let clangPreprocessorSpec: ClangCompilerSpec
package let clangStaticAnalyzerSpec: ClangCompilerSpec
package let entityLinkerToolSpec: CommandLineToolSpec
package let ssafAnalyzerToolSpec: CommandLineToolSpec
package let clangModuleVerifierSpec: ClangCompilerSpec
package let diffSpec: CommandLineToolSpec
package let stripSpec: StripToolSpec
Expand Down
7 changes: 7 additions & 0 deletions Sources/SWBUniversalPlatform/Specs/Clang.xcspec
Original file line number Diff line number Diff line change
Expand Up @@ -3482,6 +3482,13 @@
{ Name = EXTRACT_SUMMARIES;
Type = String;
DefaultValue = "";
Condition = "$(INVOKE_SSAF)";
Category = "SAPolicy";
},
{ Name = STOP_AT_LU_SUMMARY_GENERATION;
Type = StringList;
DefaultValue = "";
Condition = "$(INVOKE_SSAF)";
Category = "SAPolicy";
},
// This entry exists for string expansion in 'RuleName'.
Expand Down
106 changes: 106 additions & 0 deletions Tests/SWBBuildSystemTests/SwiftBuildOperationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,112 @@ fileprivate struct SwiftBuildOperationTests: CoreBasedTests {
}
}

@Test(.requireSDKs(.host), .requireClangFeatures(.invokeSsaf))
func invokeSsafCommandLineFlagsUnsafeBuffer() async throws {
func makeTestWorkspace(_ tmpDirPath: Path, invokeSSAF: String, extractSummaries: String = "") -> TestWorkspace {
TestWorkspace(
"Test",
sourceRoot: tmpDirPath.join("Test"),
projects: [
TestProject(
"aProject",
groupTree: TestGroup("Sources", children: [TestFile("File1.cpp")]),
buildConfigurations: [TestBuildConfiguration(
"Debug",
buildSettings: [
"PRODUCT_NAME": "$(TARGET_NAME)",
"INVOKE_SSAF": invokeSSAF,
"EXTRACT_SUMMARIES": extractSummaries,
// Uncomment to test with a local build of clang
// "CC": "<LOCAL_CLANG_PATH>/bin/clang",
"CODE_SIGNING_ALLOWED": "NO",
])],
targets: [
TestStandardTarget(
"Test",
type: .dynamicLibrary,
buildPhases: [TestSourcesBuildPhase(["File1.cpp"])])
])
])
}

// INVOKE_SSAF=YES: both flags are present and the summary file path is co-located with
// the object file, sharing the same basename but with a .json extension.
try await withTemporaryDirectory { tmpDirPath in
let tester = try await BuildOperationTester(getCore(), makeTestWorkspace(tmpDirPath, invokeSSAF: "YES", extractSummaries: "UnsafeBufferUsage"), simulated: false)
try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/File1.cpp")) {
$0 <<< "inline int shared_inline(int *p) {\n"
$0 <<< " int * l = p;\n"
$0 <<< " return l[5];\n"
$0 <<< "}\n"
$0 <<< "\n"
$0 <<< "int f(int *x) {\n"
$0 <<< " return shared_inline(x);\n"
$0 <<< "}\n"
$0 <<< "int g(int *y) {\n"
$0 <<< " return shared_inline(y);\n"
$0 <<< "}\n"
}
try await tester.checkBuild(runDestination: .host) { results in
try results.checkTask(.matchRuleType("CompileC")) { task throws in
let objectPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".o") })
let expectedJsonPath = objectPath.dirname.join(objectPath.basenameWithoutSuffix + ".ssaf-tu.json").str
task.checkCommandLineContains(["--ssaf-extract-summaries=UnsafeBufferUsage"])
task.checkCommandLineContains(["--ssaf-tu-summary-file=\(expectedJsonPath)"])

let jsonBytes = try tester.fs.read(Path(expectedJsonPath))
#expect(!jsonBytes.isEmpty)
}
// The entity linker should receive File1.ssaf-tu.json as input and produce a .linked-summaries.json file.
try results.checkTask(.matchRuleType("LinkEntity")) { task throws in
#expect(task.inputPaths.contains(where: { $0.str.hasSuffix("File1.ssaf-tu.json") }))
let linkedSummaryPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".linked-summaries.json") })
#expect(tester.fs.exists(linkedSummaryPath))
let linkedSummaryBytes = try tester.fs.read(linkedSummaryPath)
#expect(!linkedSummaryBytes.isEmpty)
}
// The analyzer should receive the .linked-summaries.json as input, pass -a UnsafeBufferReachableAnalysisResult,
// and produce a non-empty .ssaf-analysis.json file.
try results.checkTask(.matchRuleType("AnalyzeSSAF")) { task throws in
#expect(task.inputPaths.contains(where: { $0.str.hasSuffix(".linked-summaries.json") }))
task.checkCommandLineContains(["-a", "UnsafeBufferUsageAnalysisResult"])
let analysisPath = try #require(task.outputPaths.first { $0.str.hasSuffix(".ssaf-analysis.json") })
#expect(tester.fs.exists(analysisPath))
let analysisBytes = try tester.fs.read(analysisPath)
#expect(!analysisBytes.isEmpty)
}
results.checkNoDiagnostics()
}
}

// INVOKE_SSAF=NO: neither SSAF flag is present and no entity linker or analyzer task is created.
try await withTemporaryDirectory { tmpDirPath in
let tester = try await BuildOperationTester(getCore(), makeTestWorkspace(tmpDirPath, invokeSSAF: "NO"), simulated: false)
try await tester.fs.writeFileContents(tmpDirPath.join("Test/aProject/File1.cpp")) {
$0 <<< "inline int shared_inline(int *p) {\n"
$0 <<< " int * l = p;\n"
$0 <<< " return l[5];\n"
$0 <<< "}\n"
$0 <<< "\n"
$0 <<< "int f(int *x) {\n"
$0 <<< " return shared_inline(x);\n"
$0 <<< "}\n"
$0 <<< "int g(int *y) {\n"
$0 <<< " return shared_inline(y);\n"
$0 <<< "}\n"
}
try await tester.checkBuild(runDestination: .host) { results in
results.checkTask(.matchRuleType("CompileC")) { task in
task.checkCommandLineNoMatch([.prefix("--ssaf-extract-summaries=")])
task.checkCommandLineNoMatch([.prefix("--ssaf-tu-summary-file=")])
}
results.checkNoTask(.matchRuleType("LinkEntity"))
results.checkNoTask(.matchRuleType("AnalyzeSSAF"))
results.checkNoDiagnostics()
}
}
}

@Test(.requireSDKs(.host))
func avoidEmitModuleSourceInfo() async throws {
try await withTemporaryDirectory { tmpDirPath async throws -> Void in
Expand Down
Loading