Skip to content
Merged
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
22 changes: 20 additions & 2 deletions Sources/SWBCore/DependencyResolution.swift
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ struct SpecializationParameters: Hashable, CustomStringConvertible {
BuiltinMacros.SDK_VARIANT.name,
BuiltinMacros.SUPPORTED_PLATFORMS.name,
BuiltinMacros.TOOLCHAINS.name,
BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE.name,
]
@preconcurrency @PluginExtensionSystemActor func sdkVariantInfoExtensions() -> [any SDKVariantInfoExtensionPoint.ExtensionProtocol] {
core.pluginManager.extensions(of: SDKVariantInfoExtensionPoint.self)
Expand Down Expand Up @@ -137,6 +138,8 @@ struct SpecializationParameters: Hashable, CustomStringConvertible {
let toolchain: [String]?
/// Whether or not to use a suffixed SDK.
let canonicalNameSuffix: String?
/// Whether or not to enable Swift compilation cache.
let swiftCompileCache: Bool?

// Other properties.

Expand Down Expand Up @@ -227,16 +230,20 @@ struct SpecializationParameters: Hashable, CustomStringConvertible {
if let toolchain = effectiveToolchainOverride(originalParameters: parameters, workspaceContext: workspaceContext) {
overrides["TOOLCHAINS"] = toolchain.joined(separator: " ")
}
if swiftCompileCache == true {
overrides[BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE.name] = "YES"
}
return parameters.mergingOverrides(overrides)
}

init(source: SpecializationSource, platform: Platform?, sdkVariant: SDKVariant?, supportedPlatforms: [String]?, toolchain: [String]?, canonicalNameSuffix: String?, superimposedProperties: SuperimposedProperties? = nil, diagnostics: [Diagnostic] = []) {
init(source: SpecializationSource, platform: Platform?, sdkVariant: SDKVariant?, supportedPlatforms: [String]?, toolchain: [String]?, canonicalNameSuffix: String?, swiftCompileCache: Bool? = nil, superimposedProperties: SuperimposedProperties? = nil, diagnostics: [Diagnostic] = []) {
self.source = source
self.platform = platform
self.sdkVariant = sdkVariant
self.supportedPlatforms = supportedPlatforms
self.toolchain = toolchain
self.canonicalNameSuffix = canonicalNameSuffix
self.swiftCompileCache = swiftCompileCache
self.superimposedProperties = superimposedProperties
self.diagnostics = diagnostics
}
Expand Down Expand Up @@ -952,7 +959,18 @@ extension SpecializationParameters {
}

let fromPackage = workspaceContext.workspace.project(for: forTarget).isPackage
let filteredSpecialization = SpecializationParameters(source: .synthesized, platform: imposedPlatform, sdkVariant: imposedSdkVariant, supportedPlatforms: imposedSupportedPlatforms, toolchain: imposedToolchain, canonicalNameSuffix: imposedCanonicalNameSuffix, superimposedProperties: specialization.superimposedProperties)

let imposedSwiftCompileCache: Bool?
if fromPackage {
imposedSwiftCompileCache = settings.globalScope.evaluate(BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE) || buildRequest.buildTargets.contains { buildTargetInfo in
let buildTargetSettings = buildRequestContext.getCachedSettings(buildTargetInfo.parameters, target: buildTargetInfo.target)
return buildTargetSettings.globalScope.evaluate(BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE)
}
} else {
imposedSwiftCompileCache = nil
}

let filteredSpecialization = SpecializationParameters(source: .synthesized, platform: imposedPlatform, sdkVariant: imposedSdkVariant, supportedPlatforms: imposedSupportedPlatforms, toolchain: imposedToolchain, canonicalNameSuffix: imposedCanonicalNameSuffix, swiftCompileCache: imposedSwiftCompileCache, superimposedProperties: specialization.superimposedProperties)

// Otherwise, we need to create a new specialization; do so by imposing the specialization on the build parameters.
// NOTE: If the target doesn't support specialization, then unless the target comes from a package, then it's important to **not** impart those settings unless they are coming from overrides. Doing so has the side-effect of causing dependencies of downstream targets to be specialized incorrectly (e.g. a specialized target shouldn't cause its own dependencies to be specialized).
Expand Down
166 changes: 156 additions & 10 deletions Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,145 @@ fileprivate struct SwiftCompilationCachingTests: CoreBasedTests {
}
}

@Test(.requireSDKs(.iOS))
func swiftCachingSwiftPM() async throws {
try await withTemporaryDirectory { tmpDirPath async throws -> Void in
let commonBuildSettings = try await [
"SDKROOT": "auto",
"SDK_VARIANT": "auto",
"SUPPORTED_PLATFORMS": "$(AVAILABLE_PLATFORMS)",
"SWIFT_VERSION": swiftVersion,
"CODE_SIGNING_ALLOWED": "NO",
]

let leafPackage = TestPackageProject(
"aPackageLeaf",
groupTree: TestGroup("Sources", children: [TestFile("Bar.swift")]),
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: commonBuildSettings)],
targets: [
TestPackageProductTarget(
"BarProduct",
frameworksBuildPhase: TestFrameworksBuildPhase([TestBuildFile(.target("Bar"))]),
dependencies: ["Bar"]),
TestStandardTarget(
"Bar",
type: .dynamicLibrary,
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: ["PRODUCT_NAME": "Bar", "EXECUTABLE_PREFIX": "lib"])],
buildPhases: [TestSourcesBuildPhase(["Bar.swift"])])])

let package = TestPackageProject(
"aPackage",
groupTree: TestGroup("Sources", children: [TestFile("Foo.swift")]),
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: commonBuildSettings.addingContents(of: [
"SWIFT_INCLUDE_PATHS": "$(TARGET_BUILD_DIR)/../../../aPackageLeaf/build/Debug",
]))],
targets: [
TestPackageProductTarget(
"FooProduct",
frameworksBuildPhase: TestFrameworksBuildPhase([TestBuildFile(.target("Foo"))]),
dependencies: ["Foo"]),
TestStandardTarget(
"Foo",
type: .dynamicLibrary,
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: ["PRODUCT_NAME": "Foo", "EXECUTABLE_PREFIX": "lib"])],
buildPhases: [
TestSourcesBuildPhase(["Foo.swift"]),
TestFrameworksBuildPhase([TestBuildFile(.target("BarProduct"))])],
dependencies: ["BarProduct"])])

let project = TestProject(
"aProject",
groupTree: TestGroup("Sources", children: [TestFile("App1.swift"), TestFile("App2.swift")]),
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: commonBuildSettings.addingContents(of: [
"SWIFT_INCLUDE_PATHS": "$(TARGET_BUILD_DIR)/../../../aPackage/build/Debug $(TARGET_BUILD_DIR)/../../../aPackageLeaf/build/Debug"]))],
targets: [
TestStandardTarget(
"App1",
type: .framework,
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: [
"PRODUCT_NAME": "$(TARGET_NAME)",
"SWIFT_ENABLE_COMPILE_CACHE": "YES",
"COMPILATION_CACHE_ENABLE_DIAGNOSTIC_REMARKS": "YES",
"COMPILATION_CACHE_CAS_PATH": "$(DSTROOT)/CompilationCache"])],
buildPhases: [
TestSourcesBuildPhase(["App1.swift"]),
TestFrameworksBuildPhase([TestBuildFile(.target("FooProduct"))])],
dependencies: ["FooProduct"]),
TestStandardTarget(
"App2",
type: .framework,
buildConfigurations: [TestBuildConfiguration("Debug", buildSettings: [
"PRODUCT_NAME": "$(TARGET_NAME)"])],
buildPhases: [
TestSourcesBuildPhase(["App2.swift"]),
TestFrameworksBuildPhase([TestBuildFile(.target("FooProduct"))])],
dependencies: ["FooProduct"])])

let workspace = TestWorkspace("aWorkspace", sourceRoot: tmpDirPath.join("Test"), projects: [project, package, leafPackage])

let tester = try await BuildOperationTester(getCore(), workspace, simulated: false)

try await tester.fs.writeFileContents(workspace.sourceRoot.join("aPackageLeaf/Bar.swift")) { stream in
stream <<<
"""
public func baz() {}
"""
}

try await tester.fs.writeFileContents(workspace.sourceRoot.join("aPackage/Foo.swift")) { stream in
stream <<<
"""
import Bar
public func foo() { baz() }
"""
}

try await tester.fs.writeFileContents(workspace.sourceRoot.join("aProject/App1.swift")) { stream in
stream <<<
"""
import Foo
func app() { foo() }
"""
}

try await tester.fs.writeFileContents(workspace.sourceRoot.join("aProject/App2.swift")) { stream in
stream <<<
"""
import Foo
func app() { foo() }
"""
}

let parameters = BuildParameters(configuration: "Debug", overrides: ["ARCHS": "arm64"])
let buildApp1Target = BuildRequest.BuildTargetInfo(parameters: parameters, target: tester.workspace.projects[0].targets[0])
let buildApp2Target = BuildRequest.BuildTargetInfo(parameters: parameters, target: tester.workspace.projects[0].targets[1])
let buildRequest = BuildRequest(parameters: parameters, buildTargets: [buildApp2Target, buildApp1Target], continueBuildingAfterErrors: false, useParallelTargets: false, useImplicitDependencies: false, useDryRun: false)

try await tester.checkBuild(runDestination: .macOS, buildRequest: buildRequest, persistent: true) { results in
results.checkNoDiagnostics()

results.checkTasks(.matchRule(["SwiftCompile", "normal", "arm64", "Compiling Bar.swift", tmpDirPath.join("Test/aPackageLeaf/Bar.swift").str])) { tasks in
#expect(tasks.count == 1)
for task in tasks {
results.checkKeyQueryCacheMiss(task)
}
}

results.checkTask(.matchRule(["SwiftCompile", "normal", "arm64", "Compiling Foo.swift", tmpDirPath.join("Test/aPackage/Foo.swift").str])) { task in
results.checkKeyQueryCacheMiss(task)
}

results.checkTask(.matchRule(["SwiftCompile", "normal", "arm64", "Compiling App1.swift", tmpDirPath.join("Test/aProject/App1.swift").str])) { task in
results.checkKeyQueryCacheMiss(task)
}

results.checkTask(.matchRule(["SwiftCompile", "normal", "arm64", "Compiling App2.swift", "\(tmpDirPath.str)/Test/aProject/App2.swift"])) { task in
results.checkNotCached(task)
}
}
}
}

@Test(.requireSDKs(.macOS))
func swiftCASLimiting() async throws {
try await withTemporaryDirectory { (tmpDirPath: Path) async throws -> Void in
Expand Down Expand Up @@ -273,21 +412,28 @@ fileprivate struct SwiftCompilationCachingTests: CoreBasedTests {
}

extension BuildOperationTester.BuildResults {
fileprivate func checkNotCached(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) {
check(notContains: .taskHadEvent(task, event: .hadOutput(contents: "Cache miss\n")), sourceLocation: sourceLocation)
check(notContains: .taskHadEvent(task, event: .hadOutput(contents: "Cache hit\n")), sourceLocation: sourceLocation)
}

fileprivate func checkKeyQueryCacheMiss(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) {
let found = (getDiagnosticMessageForTask(.contains("cache miss"), kind: .note, task: task) != nil)
guard found else {
Issue.record("Unable to find cache miss diagnostic for task \(task)", sourceLocation: sourceLocation)
return
}
// FIXME: This doesn't work as expected (at least for Swift package targets).
// let found = (getDiagnosticMessageForTask(.contains("cache miss"), kind: .note, task: task) != nil)
// guard found else {
// Issue.record("Unable to find cache miss diagnostic for task \(task)", sourceLocation: sourceLocation)
// return
// }
check(contains: .taskHadEvent(task, event: .hadOutput(contents: "Cache miss\n")), sourceLocation: sourceLocation)
}

fileprivate func checkKeyQueryCacheHit(_ task: Task, sourceLocation: SourceLocation = #_sourceLocation) {
let found = (getDiagnosticMessageForTask(.contains("cache found for key"), kind: .note, task: task) != nil)
guard found else {
Issue.record("Unable to find cache hit diagnostic for task \(task)", sourceLocation: sourceLocation)
return
}
// FIXME: This doesn't work as expected (at least for Swift package targets).
// let found = (getDiagnosticMessageForTask(.contains("cache found for key"), kind: .note, task: task) != nil)
// guard found else {
// Issue.record("Unable to find cache hit diagnostic for task \(task)", sourceLocation: sourceLocation)
// return
// }
check(contains: .taskHadEvent(task, event: .hadOutput(contents: "Cache hit\n")), sourceLocation: sourceLocation)
}
}
Loading