From 1265093e72b34a55d5a747589f79d91c74cd9fa4 Mon Sep 17 00:00:00 2001 From: Bassam Khouri Date: Thu, 6 Nov 2025 14:26:13 -0500 Subject: [PATCH 1/2] SwiftBuild: Add index store support Update the PIF builder to take into account of the `--[auto|disable|enable]-index-store` command line option. When set to `auto`, the PIF builder behaviour does the same as the Native build system. Fixes: #9325 Issue: rdar://163961900 --- Fixtures/PIFBuilder/Simple/.gitignore | 8 + Fixtures/PIFBuilder/Simple/Package.swift | 26 +++ .../Simple/Sources/Simple/Simple.swift | 25 +++ .../Tests/SimpleTests/SimpleTests.swift | 36 ++++ Sources/PackageModel/SwiftSDKs/SwiftSDK.swift | 3 +- .../BuildParameters/BuildParameters.swift | 2 +- .../SwiftBuildSupport/PackagePIFBuilder.swift | 13 +- .../SwiftBuildSupport/SwiftBuildSystem.swift | 70 ++++++-- .../MockBuildTestHelper.swift | 2 +- .../SwiftTesting+Tags.swift | 6 + Sources/_InternalTestSupport/Toolchain.swift | 4 +- .../PIFBuilderTests.swift | 113 ++++++++++-- .../SwiftBuildSystemTests.swift | 161 ++++++++++++++++++ 13 files changed, 426 insertions(+), 43 deletions(-) create mode 100644 Fixtures/PIFBuilder/Simple/.gitignore create mode 100644 Fixtures/PIFBuilder/Simple/Package.swift create mode 100644 Fixtures/PIFBuilder/Simple/Sources/Simple/Simple.swift create mode 100644 Fixtures/PIFBuilder/Simple/Tests/SimpleTests/SimpleTests.swift create mode 100644 Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift diff --git a/Fixtures/PIFBuilder/Simple/.gitignore b/Fixtures/PIFBuilder/Simple/.gitignore new file mode 100644 index 00000000000..0023a534063 --- /dev/null +++ b/Fixtures/PIFBuilder/Simple/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Fixtures/PIFBuilder/Simple/Package.swift b/Fixtures/PIFBuilder/Simple/Package.swift new file mode 100644 index 00000000000..4449f7edfff --- /dev/null +++ b/Fixtures/PIFBuilder/Simple/Package.swift @@ -0,0 +1,26 @@ +// swift-tools-version: 6.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Simple", + products: [ + // Products define the executables and libraries a package produces, making them visible to other packages. + .library( + name: "Simple", + targets: ["Simple"] + ), + ], + targets: [ + // Targets are the basic building blocks of a package, defining a module or a test suite. + // Targets can depend on other targets in this package and products from dependencies. + .target( + name: "Simple" + ), + .testTarget( + name: "SimpleTests", + dependencies: ["Simple"] + ), + ] +) diff --git a/Fixtures/PIFBuilder/Simple/Sources/Simple/Simple.swift b/Fixtures/PIFBuilder/Simple/Sources/Simple/Simple.swift new file mode 100644 index 00000000000..551df789c2f --- /dev/null +++ b/Fixtures/PIFBuilder/Simple/Sources/Simple/Simple.swift @@ -0,0 +1,25 @@ +// The Swift Programming Language +// https://docs.swift.org/swift-book + +public struct Person { + public let name: String + + public init(name: String) { + self.name = name + } +} +extension Person: CustomStringConvertible { + public var description: String { + return name + } +} + +public func greet(person: Person? = nil) -> String { + let name = if let person { + person.name + } else { + "World" + } + + return "Hello, \(name)!" +} diff --git a/Fixtures/PIFBuilder/Simple/Tests/SimpleTests/SimpleTests.swift b/Fixtures/PIFBuilder/Simple/Tests/SimpleTests/SimpleTests.swift new file mode 100644 index 00000000000..55a56f80377 --- /dev/null +++ b/Fixtures/PIFBuilder/Simple/Tests/SimpleTests/SimpleTests.swift @@ -0,0 +1,36 @@ +import Testing +import XCTest + +import Simple + + +final public class XCTesting: XCTestCase { + func testGreetWithEmptyArgument() { + let actual = greet() + XCTAssertEqual(actual, "Hello, World!") + } + + func testGreetWithNonEmptyArgument() { + let name = "MyName" + let person = Person(name: name) + let actual = greet(person: person) + XCTAssertEqual(actual, "Hello, \(name)!") + } +} + +@Suite +struct STTestTests { + @Test("STTest tests") + func testGreetWithEmptyArgument() { + let actual = greet() + #expect(actual == "Hello, World!") + } + + @Test("STTest tests") + func testGreetWithNonEmptyArgument() { + let name = "MyName" + let person = Person(name: name) + let actual = greet(person: person) + #expect(actual == "Hello, \(name)!") + } +} diff --git a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift index bef655063af..ab14ddb019b 100644 --- a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift +++ b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift @@ -609,7 +609,7 @@ public struct SwiftSDK: Equatable { extraCCFlags += ["-fPIC"] #endif - return SwiftSDK( + let returnValue = SwiftSDK( toolset: .init( knownTools: [ .cCompiler: .init(extraCLIOptions: extraCCFlags), @@ -620,6 +620,7 @@ public struct SwiftSDK: Equatable { pathsConfiguration: .init(sdkRootPath: sdkPath), xctestSupport: xctestSupport ) + return returnValue } /// Auxiliary platform frameworks and libraries. diff --git a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift index e5d9802b79f..a9fe2744794 100644 --- a/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift +++ b/Sources/SPMBuildCore/BuildParameters/BuildParameters.swift @@ -29,7 +29,7 @@ public struct BuildParameters: Encodable { } /// Mode for the indexing-while-building feature. - public enum IndexStoreMode: String, Encodable { + public enum IndexStoreMode: String, Encodable, CaseIterable { /// Index store should be enabled. case on /// Index store should be disabled. diff --git a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift index 53c7a428be8..90ff3db30e0 100644 --- a/Sources/SwiftBuildSupport/PackagePIFBuilder.swift +++ b/Sources/SwiftBuildSupport/PackagePIFBuilder.swift @@ -32,6 +32,8 @@ import struct PackageGraph.ModulesGraph import struct PackageGraph.ResolvedModule import struct PackageGraph.ResolvedPackage +import struct SPMBuildCore.BuildParameters + import enum SwiftBuild.ProjectModel typealias GUID = SwiftBuild.ProjectModel.GUID @@ -203,7 +205,7 @@ public final class PackagePIFBuilder { addLocalRpaths: Bool = true, packageDisplayVersion: String?, fileSystem: FileSystem, - observabilityScope: ObservabilityScope + observabilityScope: ObservabilityScope, ) { self.package = resolvedPackage self.packageManifest = packageManifest @@ -227,7 +229,7 @@ public final class PackagePIFBuilder { addLocalRpaths: Bool = true, packageDisplayVersion: String?, fileSystem: FileSystem, - observabilityScope: ObservabilityScope + observabilityScope: ObservabilityScope, ) { self.package = resolvedPackage self.packageManifest = packageManifest @@ -438,7 +440,7 @@ public final class PackagePIFBuilder { // self.log(.debug, "Processing \(package.products.count) products:") - + // For each of the **products** in the package we create a corresponding `PIFTarget` of the appropriate type. for product in self.package.products { switch product.type { @@ -561,8 +563,8 @@ public final class PackagePIFBuilder { // We currently deliberately do not support Swift ObjC interface headers. settings[.SWIFT_INSTALL_OBJC_HEADER] = "NO" settings[.SWIFT_OBJC_INTERFACE_HEADER_NAME] = "" - - // rdar://47937899 (Don't try to link frameworks to object files) + + // rdar://47937899 (Don't try to link frameworks to object files) // - looks like this defaults to OTHER_LDFLAGS (via xcspec) which can result in linking frameworks to mh_objects which is unwanted. settings[.OTHER_LDRFLAGS] = [] @@ -622,6 +624,7 @@ public final class PackagePIFBuilder { debugSettings[.ENABLE_TESTABILITY] = "YES" debugSettings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS, default: []].append(contentsOf: ["DEBUG"]) debugSettings[.GCC_PREPROCESSOR_DEFINITIONS, default: ["$(inherited)"]].append(contentsOf: ["DEBUG=1"]) + debugSettings[.SWIFT_INDEX_STORE_ENABLE] = "YES" builder.project.addBuildConfig { id in BuildConfig(id: id, name: "Debug", settings: debugSettings) } // Add the build settings that are specific to release builds, and set those as the "Release" configuration. diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index bf0e98161cf..c92a598ea2c 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -43,7 +43,7 @@ struct SessionFailedError: Error { var diagnostics: [SwiftBuild.SwiftBuildMessage.DiagnosticInfo] } -func withService( +package func withService( connectionMode: SWBBuildServiceConnectionMode = .default, variant: SWBBuildServiceVariant = .default, serviceBundleURL: URL? = nil, @@ -860,7 +860,11 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { ) } - private func makeBuildParameters(session: SWBBuildServiceSession, symbolGraphOptions: BuildOutput.SymbolGraphOptions?) async throws -> SwiftBuild.SWBBuildParameters { + internal func makeBuildParameters( + session: SWBBuildServiceSession, + symbolGraphOptions: BuildOutput.SymbolGraphOptions?, + setToolchainSetting: Bool = true, + ) async throws -> SwiftBuild.SWBBuildParameters { // Generate the run destination parameters. let runDestination = makeRunDestination() @@ -872,17 +876,19 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { // Generate a table of any overriding build settings. var settings: [String: String] = [:] - // If the SwiftPM toolchain corresponds to a toolchain registered with the lower level build system, add it to the toolchain stack. - // Otherwise, apply overrides for each component of the SwiftPM toolchain. - if let toolchainID = try await session.lookupToolchain(at: buildParameters.toolchain.toolchainDir.pathString) { - settings["TOOLCHAINS"] = "\(toolchainID.rawValue) $(inherited)" - } else { - // FIXME: This list of overrides is incomplete. - // An error with determining the override should not be fatal here. - settings["CC"] = try? buildParameters.toolchain.getClangCompiler().pathString - // Always specify the path of the effective Swift compiler, which was determined in the same way as for the - // native build system. - settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString + if setToolchainSetting { + // If the SwiftPM toolchain corresponds to a toolchain registered with the lower level build system, add it to the toolchain stack. + // Otherwise, apply overrides for each component of the SwiftPM toolchain. + if let toolchainID = try await session.lookupToolchain(at: buildParameters.toolchain.toolchainDir.pathString) { + settings["TOOLCHAINS"] = "\(toolchainID.rawValue) $(inherited)" + } else { + // FIXME: This list of overrides is incomplete. + // An error with determining the override should not be fatal here. + settings["CC"] = try? buildParameters.toolchain.getClangCompiler().pathString + // Always specify the path of the effective Swift compiler, which was determined in the same way as for the + // native build system. + settings["SWIFT_EXEC"] = buildParameters.toolchain.swiftCompilerPath.pathString + } } // FIXME: workaround for old Xcode installations such as what is in CI @@ -985,6 +991,38 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { settings["GENERATE_TEST_ENTRYPOINTS_FOR_BUNDLES"] = "YES" } + // Set the value of the index store + struct IndexStoreSettings { + let enableVariableName: String + let pathVariable: String + } + + let indexStoreSettingNames: [IndexStoreSettings] = [ + IndexStoreSettings( + enableVariableName: "CLANG_INDEX_STORE_ENABLE", + pathVariable: "CLANG_INDEX_STORE_PATH", + ), + IndexStoreSettings( + enableVariableName: "SWIFT_INDEX_STORE_ENABLE", + pathVariable: "SWIFT_INDEX_STORE_PATH", + ), + ] + + switch self.buildParameters.indexStoreMode { + case .on: + for setting in indexStoreSettingNames { + settings[setting.enableVariableName] = "YES" + settings[setting.pathVariable] = self.buildParameters.indexStore.pathString + } + case .off: + for setting in indexStoreSettingNames { + settings[setting.enableVariableName] = "NO" + } + case .auto: + // The settings are handles in the PIF builder + break + } + func reportConflict(_ a: String, _ b: String) throws -> String { throw StringError("Build parameters constructed conflicting settings overrides '\(a)' and '\(b)'") } @@ -1223,13 +1261,13 @@ fileprivate extension SwiftBuild.SwiftBuildMessage.DiagnosticInfo.Location { case .none: return path } - + case .buildSettings(let names): return names.joined(separator: ", ") - + case .buildFiles(let buildFiles, let targetGUID): return "\(targetGUID): " + buildFiles.map { String(describing: $0) }.joined(separator: ", ") - + case .unknown: return nil } diff --git a/Sources/_InternalTestSupport/MockBuildTestHelper.swift b/Sources/_InternalTestSupport/MockBuildTestHelper.swift index 024726107c9..66fc707243d 100644 --- a/Sources/_InternalTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalTestSupport/MockBuildTestHelper.swift @@ -87,7 +87,7 @@ public func mockBuildParameters( shouldDisableLocalRpath: Bool = false, canRenameEntrypointFunctionName: Bool = false, triple: Basics.Triple = hostTriple, - indexStoreMode: BuildParameters.IndexStoreMode = .off, + indexStoreMode: BuildParameters.IndexStoreMode = .auto, linkerDeadStrip: Bool = true, linkTimeOptimizationMode: BuildParameters.LinkTimeOptimizationMode? = nil, omitFramePointers: Bool? = nil, diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index 1a9a2ffd677..e5374633046 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -14,6 +14,7 @@ extension Tag { public enum TestSize {} public enum Feature {} public enum Platform {} + public enum FunctionalArea {} @Tag public static var UserWorkflow: Tag } @@ -191,3 +192,8 @@ extension Tag.Feature.Product { @Tag public static var Execute: Tag @Tag public static var Link: Tag } + +extension Tag.FunctionalArea { + @Tag public static var PIF: Tag + @Tag public static var IndexMode: Tag +} diff --git a/Sources/_InternalTestSupport/Toolchain.swift b/Sources/_InternalTestSupport/Toolchain.swift index 3bbb62cb620..378a898cf3d 100644 --- a/Sources/_InternalTestSupport/Toolchain.swift +++ b/Sources/_InternalTestSupport/Toolchain.swift @@ -53,7 +53,9 @@ extension SwiftSDK { extension UserToolchain { public static var `default`: Self { get throws { - return try .init(swiftSDK: SwiftSDK.default, environment: .current, fileSystem: localFileSystem) + let value = try Self.init(swiftSDK: SwiftSDK.default, environment: .current, fileSystem: localFileSystem) + return value + // return .init(swiftSDK: SwiftSDK.default, environment: .current, fileSystem: localFileSystem) } } } diff --git a/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift b/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift index 80f00260ee5..dc9fe6271ba 100644 --- a/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift +++ b/Tests/SwiftBuildSupportTests/PIFBuilderTests.swift @@ -43,13 +43,24 @@ extension PIFBuilderParameters { } } -fileprivate func withGeneratedPIF(fromFixture fixtureName: String, addLocalRpaths: Bool = true, do doIt: (SwiftBuildSupport.PIF.TopLevelObject, TestingObservability) async throws -> ()) async throws { +fileprivate func withGeneratedPIF( + fromFixture fixtureName: String, + addLocalRpaths: Bool = true, + buildParameters: BuildParameters? = nil, + do doIt: (SwiftBuildSupport.PIF.TopLevelObject, TestingObservability) async throws -> (), +) async throws { + let buildParameters = if let buildParameters { + buildParameters + } else { + mockBuildParameters(destination: .host) + } try await fixture(name: fixtureName) { fixturePath in - let observabilitySystem = ObservabilitySystem.makeForTesting() + let observabilitySystem: TestingObservability = ObservabilitySystem.makeForTesting() + let toolchain = try UserToolchain.default let workspace = try Workspace( fileSystem: localFileSystem, forRootPackage: fixturePath, - customManifestLoader: ManifestLoader(toolchain: UserToolchain.default), + customManifestLoader: ManifestLoader(toolchain: toolchain), delegate: MockWorkspaceDelegate() ) let rootInput = PackageGraphRootInput(packages: [fixturePath], dependencies: []) @@ -64,7 +75,7 @@ fileprivate func withGeneratedPIF(fromFixture fixtureName: String, addLocalRpath observabilityScope: observabilitySystem.topScope ) let pif = try await builder.constructPIF( - buildParameters: mockBuildParameters(destination: .host) + buildParameters: buildParameters, ) try await doIt(pif, observabilitySystem) } @@ -119,9 +130,9 @@ extension SwiftBuildSupport.PIF.Project { } } - fileprivate func buildConfig(named name: String) throws -> SwiftBuild.ProjectModel.BuildConfig { + fileprivate func buildConfig(named name: BuildConfiguration) throws -> SwiftBuild.ProjectModel.BuildConfig { let matchingConfigs = underlying.buildConfigs.filter { - $0.name == name + $0.name == name.rawValue.capitalized } if matchingConfigs.isEmpty { throw StringError("No config named \(name) in PIF project") @@ -134,9 +145,9 @@ extension SwiftBuildSupport.PIF.Project { } extension SwiftBuild.ProjectModel.BaseTarget { - fileprivate func buildConfig(named name: String) throws -> SwiftBuild.ProjectModel.BuildConfig { + fileprivate func buildConfig(named name: BuildConfiguration) throws -> SwiftBuild.ProjectModel.BuildConfig { let matchingConfigs = common.buildConfigs.filter { - $0.name == name + $0.name == name.pifConfiguration } if matchingConfigs.isEmpty { throw StringError("No config named \(name) in PIF target") @@ -148,7 +159,20 @@ extension SwiftBuild.ProjectModel.BaseTarget { } } -@Suite +extension BuildConfiguration { + var pifConfiguration: String { + switch self { + case .debug, .release: self.rawValue.capitalized + } + } +} + +@Suite( + .tags( + .TestSize.medium, + .FunctionalArea.PIF, + ), +) struct PIFBuilderTests { @Test func platformConditionBasics() async throws { try await withGeneratedPIF(fromFixture: "PIFBuilder/UnknownPlatforms") { pif, observabilitySystem in @@ -160,7 +184,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "UnknownPlatforms") .target(named: "UnknownPlatforms") - .buildConfig(named: "Release") + .buildConfig(named: .release) // The platforms with conditional settings should have those propagated to the PIF. #expect(releaseConfig.settings[.SWIFT_ACTIVE_COMPILATION_CONDITIONS, .linux] == ["$(inherited)", "BAR"]) @@ -174,7 +198,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "CCPackage") .target(id: "PACKAGE-TARGET:CCTarget") - .buildConfig(named: "Release") + .buildConfig(named: .release) for platform in ProjectModel.BuildSettings.Platform.allCases { let ld_flags = releaseConfig.impartedBuildProperties.settings[.OTHER_LDFLAGS, platform] @@ -182,7 +206,7 @@ struct PIFBuilderTests { case .macOS, .macCatalyst, .iOS, .watchOS, .tvOS, .xrOS, .driverKit, .freebsd: #expect(ld_flags == ["-lc++", "$(inherited)"], "for platform \(platform)") case .android, .linux, .wasi, .openbsd: - #expect(ld_flags == ["-lstdc++", "$(inherited)"], "for platform \(platform)") + #expect(ld_flags == ["-lstdc++", "$(inherited)"], "for platform \(platform)") case .windows, ._iOSDevice: #expect(ld_flags == nil, "for platform \(platform)") } @@ -197,7 +221,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "PackageWithSDKSpecialization") - .buildConfig(named: "Release") + .buildConfig(named: .release) #expect(releaseConfig.settings[.SPECIALIZATION_SDK_OPTIONS, .macOS] == ["foo"]) } @@ -241,7 +265,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "ModuleMapGenerationCases") .target(named: "UmbrellaHeader") - .buildConfig(named: "Release") + .buildConfig(named: .release) #expect(releaseConfig.impartedBuildProperties.settings[.OTHER_CFLAGS] == ["-fmodule-map-file=\(RelativePath("$(GENERATED_MODULEMAP_DIR)").appending(component: "UmbrellaHeader.modulemap").pathString)", "$(inherited)"]) } @@ -250,7 +274,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "ModuleMapGenerationCases") .target(named: "UmbrellaDirectoryInclude") - .buildConfig(named: "Release") + .buildConfig(named: .release) #expect(releaseConfig.impartedBuildProperties.settings[.OTHER_CFLAGS] == ["-fmodule-map-file=\(RelativePath("$(GENERATED_MODULEMAP_DIR)").appending(component: "UmbrellaDirectoryInclude.modulemap").pathString)", "$(inherited)"]) } @@ -259,7 +283,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "ModuleMapGenerationCases") .target(named: "CustomModuleMap") - .buildConfig(named: "Release") + .buildConfig(named: .release) let arg = try #require(releaseConfig.impartedBuildProperties.settings[.OTHER_CFLAGS]?.first) #expect(arg.hasPrefix("-fmodule-map-file") && arg.hasSuffix(RelativePath("CustomModuleMap").appending(components: ["include", "module.modulemap"]).pathString)) } @@ -276,7 +300,7 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "Foo") .target(named: "Foo") - .buildConfig(named: "Release") + .buildConfig(named: .release) #expect(releaseConfig.impartedBuildProperties.settings[.LD_RUNPATH_SEARCH_PATHS] == ["$(RPATH_ORIGIN)", "$(inherited)"]) } @@ -291,10 +315,63 @@ struct PIFBuilderTests { let releaseConfig = try pif.workspace .project(named: "Foo") .target(named: "Foo") - .buildConfig(named: "Release") + .buildConfig(named: .release) #expect(releaseConfig.impartedBuildProperties.settings[.LD_RUNPATH_SEARCH_PATHS] == nil) } } } + + @Suite( + .tags( + .FunctionalArea.IndexMode, + ), + ) + struct IndexModeSettingTests { + + @Test( + arguments: [BuildParameters.IndexStoreMode.auto], [BuildConfiguration.debug], + // arguments: BuildParameters.IndexStoreMode.allCases, BuildConfiguration.allCases, + ) + func indexModeSettingSetTo( + indexStoreSettingUT: BuildParameters.IndexStoreMode, + configuration: BuildConfiguration, + ) async throws { + try await withGeneratedPIF( + fromFixture: "PIFBuilder/Simple", + buildParameters: mockBuildParameters(destination: .host, indexStoreMode: indexStoreSettingUT), + ) { pif, observabilitySystem in + // #expect(false, "fail purposefully...") + #expect(observabilitySystem.diagnostics.filter { + $0.severity == .error + }.isEmpty) + + let targetConfig = try pif.workspace + .project(named: "Simple") + // .target(named: "Simple") + .buildConfig(named: configuration) + switch indexStoreSettingUT { + case .on, .off: + #expect(targetConfig.settings[.SWIFT_INDEX_STORE_ENABLE] == nil) + case .auto: + let expectedSwiftIndexStoreEnableValue: String? = switch configuration { + case .debug: "YES" + case .release: nil + } + #expect(targetConfig.settings[.SWIFT_INDEX_STORE_ENABLE] == expectedSwiftIndexStoreEnableValue) + } + + let testTargetConfig = try pif.workspace + .project(named: "Simple") + .target(named: "SimplePackageTests-product") + .buildConfig(named: configuration) + switch indexStoreSettingUT { + case .on, .off: + #expect(testTargetConfig.settings[.SWIFT_INDEX_STORE_ENABLE] == nil) + case .auto: + #expect(testTargetConfig.settings[.SWIFT_INDEX_STORE_ENABLE] == "YES") + } + } + } + } } diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift new file mode 100644 index 00000000000..469278229bb --- /dev/null +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift @@ -0,0 +1,161 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@testable import SwiftBuildSupport +import SPMBuildCore + + +import var TSCBasic.stderrStream +import Basics +import Workspace +import PackageModel +import PackageGraph +import PackageLoading + +@testable import SwiftBuild +import SWBBuildService + +import _InternalTestSupport + +func withInstantiatedSwiftBuildSystem( + fromFixture fixtureName: String, + buildParameters: BuildParameters? = nil, + logLevel: Basics.Diagnostic.Severity = .warning, + do doIt: @escaping (SwiftBuildSupport.SwiftBuildSystem, SWBBuildServiceSession, TestingObservability, BuildParameters,) async throws -> (), +) async throws { + let fileSystem = Basics.localFileSystem + + try await fixture(name: fixtureName) { fixturePath in + try await withTemporaryDirectory { tmpDir in + let buildParameters = if let buildParameters { + buildParameters + } else { + mockBuildParameters(destination: .host) + } + let observabilitySystem: TestingObservability = ObservabilitySystem.makeForTesting() + let toolchain = try UserToolchain.default + let workspace = try Workspace( + fileSystem: localFileSystem, + forRootPackage: fixturePath, + customManifestLoader: ManifestLoader(toolchain: toolchain), + ) + let rootInput = PackageGraphRootInput(packages: [fixturePath], dependencies: []) + let graphLoader = { + try await workspace.loadPackageGraph( + rootInput: rootInput, + observabilityScope: observabilitySystem.topScope + ) + } + + let pluginScriptRunner = try DefaultPluginScriptRunner( + fileSystem: fileSystem, + cacheDir: tmpDir.appending("plugin-script-cache"), + toolchain: UserToolchain.default, + ) + + let swBuild = try SwiftBuildSystem( + buildParameters: buildParameters, + packageGraphLoader: graphLoader, + packageManagerResourcesDirectory: nil, + additionalFileRules: [], + outputStream: TSCBasic.stderrStream, + logLevel: logLevel, + fileSystem: fileSystem, + observabilityScope: observabilitySystem.topScope, + pluginConfiguration: PluginConfiguration( + scriptRunner: pluginScriptRunner, + workDirectory: AbsolutePath("/tmp/plugin-script-working-dir"), + disableSandbox: true, + ), + delegate: nil, + ) + + try await SwiftBuildSupport.withService( + connectionMode: .inProcessStatic(swiftbuildServiceEntryPoint), + ) { service in + + let result = await service.createSession( + name: "session", + cachePath: nil, + inferiorProductsPath: nil, + environment: nil, + ) + + let buildSession: SWBBuildServiceSession + switch result { + case (.success(let session), _): + buildSession = session + case (.failure(let error), _): + throw StringError("\(error)") + // throw SessionFailedError(error: error, diagnostics: diagnostics) + } + + do { + try await doIt(swBuild, buildSession, observabilitySystem, buildParameters) + try await buildSession.close() + } catch { + try await buildSession.close() + throw error + } + } + } + } +} + +@Suite( + .tags( + .TestSize.medium, + ), +) +struct SwiftBuildSystemTests { + + @Test( + arguments: BuildParameters.IndexStoreMode.allCases, + // arguments: [BuildParameters.IndexStoreMode.on], + ) + func indexModeSettingSetCorrectBuildRequest( + indexStoreSettingUT: BuildParameters.IndexStoreMode + ) async throws { + try await withInstantiatedSwiftBuildSystem( + fromFixture: "PIFBuilder/Simple", + buildParameters: mockBuildParameters( + destination: .host, + indexStoreMode: indexStoreSettingUT, + ), + ) { swiftBuild, session, observabilityScope, buildParameters in + let buildSettings = try await swiftBuild.makeBuildParameters( + session: session, + symbolGraphOptions: nil, + setToolchainSetting: false, // Set this to false as SwiftBuild checks the toolchain path + ) + + let synthesizedArgs = try #require(buildSettings.overrides.synthesized) + let expectedSettingValue: String? = switch indexStoreSettingUT { + case .on: "YES" + case .off: "NO" + case .auto: nil + } + let expectedPathValue: String? = switch indexStoreSettingUT { + case .on: buildParameters.indexStore.pathString + case .off: nil + case .auto: nil + } + + #expect(synthesizedArgs.table["SWIFT_INDEX_STORE_ENABLE"] == expectedSettingValue) + #expect(synthesizedArgs.table["CLANG_INDEX_STORE_ENABLE"] == expectedSettingValue) + #expect(synthesizedArgs.table["SWIFT_INDEX_STORE_PATH"] == expectedPathValue) + #expect(synthesizedArgs.table["CLANG_INDEX_STORE_PATH"] == expectedPathValue) + } + } +} From 5e61e57241a9828a0ebc2e45eac84bddbcf502e3 Mon Sep 17 00:00:00 2001 From: Bassam Khouri Date: Sat, 15 Nov 2025 14:23:13 -0500 Subject: [PATCH 2/2] SwiftBuild Integration: Enable some sanitizers Update the SwiftPM's SwiftBuild integration to support enabling `address`, `thread` and `undefined` sanitizers while erroring out on the `scudo` and `fuzzer`. Depends on: https://github.com/swiftlang/swift-build/pull/926 --- .../SwiftBuildSupport/SwiftBuildSystem.swift | 14 ++++ .../MockBuildTestHelper.swift | 6 +- .../SwiftTesting+Tags.swift | 11 +-- .../Sanitizer+ExtensionsTests.swift | 3 +- .../SwiftBuildSystemTests.swift | 77 +++++++++++++++++++ 5 files changed, 103 insertions(+), 8 deletions(-) diff --git a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift index c92a598ea2c..fa365043c0c 100644 --- a/Sources/SwiftBuildSupport/SwiftBuildSystem.swift +++ b/Sources/SwiftBuildSupport/SwiftBuildSystem.swift @@ -891,6 +891,20 @@ public final class SwiftBuildSystem: SPMBuildCore.BuildSystem { } } + for sanitizer in buildParameters.sanitizers.sanitizers { + self.observabilityScope.emit(debug:"Enabling \(sanitizer) sanitizer") + switch sanitizer { + case .address: + settings["ENABLE_ADDRESS_SANITIZER"] = "YES" + case .thread: + settings["ENABLE_THREAD_SANITIZER"] = "YES" + case .undefined: + settings["ENABLE_UNDEFINED_BEHAVIOR_SANITIZER"] = "YES" + case .fuzzer, .scudo: + throw StringError("\(sanitizer) is not currently supported with this build system.") + } + } + // FIXME: workaround for old Xcode installations such as what is in CI settings["LM_SKIP_METADATA_EXTRACTION"] = "YES" if let symbolGraphOptions { diff --git a/Sources/_InternalTestSupport/MockBuildTestHelper.swift b/Sources/_InternalTestSupport/MockBuildTestHelper.swift index 66fc707243d..bcb24f05a18 100644 --- a/Sources/_InternalTestSupport/MockBuildTestHelper.swift +++ b/Sources/_InternalTestSupport/MockBuildTestHelper.swift @@ -92,7 +92,8 @@ public func mockBuildParameters( linkTimeOptimizationMode: BuildParameters.LinkTimeOptimizationMode? = nil, omitFramePointers: Bool? = nil, enableXCFrameworksOnLinux: Bool = false, - prepareForIndexing: BuildParameters.PrepareForIndexingMode = .off + prepareForIndexing: BuildParameters.PrepareForIndexingMode = .off, + sanitizers: [Sanitizer] = [], ) -> BuildParameters { try! BuildParameters( destination: destination, @@ -104,6 +105,7 @@ public func mockBuildParameters( buildSystemKind: buildSystemKind, pkgConfigDirectories: [], workers: 3, + sanitizers: EnabledSanitizers(Set(sanitizers)), indexStoreMode: indexStoreMode, prepareForIndexing: prepareForIndexing, enableXCFrameworksOnLinux: enableXCFrameworksOnLinux, @@ -120,7 +122,7 @@ public func mockBuildParameters( linkTimeOptimizationMode: linkTimeOptimizationMode, shouldDisableLocalRpath: shouldDisableLocalRpath, shouldLinkStaticSwiftStdlib: shouldLinkStaticSwiftStdlib - ) + ), ) } diff --git a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift index e5374633046..9bb6b5eeaf3 100644 --- a/Sources/_InternalTestSupport/SwiftTesting+Tags.swift +++ b/Sources/_InternalTestSupport/SwiftTesting+Tags.swift @@ -28,6 +28,12 @@ extension Tag.Platform { @Tag public static var FileSystem: Tag } +extension Tag.FunctionalArea { + @Tag public static var PIF: Tag + @Tag public static var IndexMode: Tag + @Tag public static var Sanitizer: Tag +} + extension Tag.Feature { public enum Command {} public enum CommandLineArguments {} @@ -192,8 +198,3 @@ extension Tag.Feature.Product { @Tag public static var Execute: Tag @Tag public static var Link: Tag } - -extension Tag.FunctionalArea { - @Tag public static var PIF: Tag - @Tag public static var IndexMode: Tag -} diff --git a/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift b/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift index c7008f269b1..6db32201f71 100644 --- a/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift +++ b/Tests/CommandsTests/Sanitizer+ExtensionsTests.swift @@ -15,7 +15,8 @@ import enum PackageModel.Sanitizer @Suite( .tags( - Tag.TestSize.small, + .TestSize.small, + .FunctionalArea.Sanitizer, ), ) struct SanitizerExtensionTests { diff --git a/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift b/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift index 469278229bb..161c94eec1a 100644 --- a/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift +++ b/Tests/SwiftBuildSupportTests/SwiftBuildSystemTests.swift @@ -113,6 +113,25 @@ func withInstantiatedSwiftBuildSystem( } } +extension PackageModel.Sanitizer { + var hasSwiftBuildSupport: Bool { + switch self { + case .address, .thread, .undefined: true + case .fuzzer, .scudo: false + } + } + + var swiftBuildSettingName: String? { + switch self { + case .address: "ENABLE_ADDRESS_SANITIZER" + case .thread: "ENABLE_THREAD_SANITIZER" + case .undefined: "ENABLE_UNDEFINED_BEHAVIOR_SANITIZER" + case .fuzzer, .scudo: nil + } + + } +} + @Suite( .tags( .TestSize.medium, @@ -120,6 +139,64 @@ func withInstantiatedSwiftBuildSystem( ) struct SwiftBuildSystemTests { + @Suite( + .tags( + .FunctionalArea.Sanitizer, + ) + ) + struct SanitizerTests { + + @Test( + arguments: PackageModel.Sanitizer.allCases.filter { $0.hasSwiftBuildSupport }, + ) + func sanitizersSettingSetCorrectBuildRequest( + sanitizer: Sanitizer, + ) async throws { + try await withInstantiatedSwiftBuildSystem( + fromFixture: "PIFBuilder/Simple", + buildParameters: mockBuildParameters( + destination: .host, + sanitizers: [sanitizer], + ), + ) { swiftBuild, session, observabilityScope, buildParameters in + let buildSettings: SWBBuildParameters = try await swiftBuild.makeBuildParameters( + session: session, + symbolGraphOptions: nil, + setToolchainSetting: false, // Set this to false as SwiftBuild checks the toolchain path + ) + + let synthesizedArgs = try #require(buildSettings.overrides.synthesized) + + let swbSettingName = try #require(sanitizer.swiftBuildSettingName) + #expect(synthesizedArgs.table[swbSettingName] == "YES") + } + + } + + @Test( + arguments: PackageModel.Sanitizer.allCases.filter { !$0.hasSwiftBuildSupport }, + ) + func unsupportedSanitizersRaisesError( + sanitizer: Sanitizer, + ) async throws { + try await withInstantiatedSwiftBuildSystem( + fromFixture: "PIFBuilder/Simple", + buildParameters: mockBuildParameters( + destination: .host, + sanitizers: [sanitizer], + ), + ) { swiftBuild, session, observabilityScope, buildParameters in + await #expect(throws: (any Error).self) { + try await swiftBuild.makeBuildParameters( + session: session, + symbolGraphOptions: nil, + setToolchainSetting: false, // Set this to false as SwiftBuild checks the toolchain path + ) + } + } + } + } + @Test( arguments: BuildParameters.IndexStoreMode.allCases, // arguments: [BuildParameters.IndexStoreMode.on],