From df775e89ebab60b68aabaff5488d2c9751d7ee67 Mon Sep 17 00:00:00 2001 From: Steven Wu Date: Sat, 13 Dec 2025 14:20:27 -0800 Subject: [PATCH] [CompilationCaching] Write .cas-config file Write a .cas-config file when compilation caching is enabled. This allows tools that might not be orchestrated by the build system directly but might need to read CAS to find the CAS by searching for the configuration file. rdar://166398186 --- Sources/SWBTaskConstruction/CMakeLists.txt | 1 + .../ProductPlanning/ProductPlanner.swift | 1 + .../CASConfigFileTaskProducer.swift | 70 +++++++++++++++++++ .../ClangCompilationCachingTests.swift | 7 ++ .../SwiftCompilationCachingTests.swift | 2 + 5 files changed, 81 insertions(+) create mode 100644 Sources/SWBTaskConstruction/TaskProducers/WorkspaceTaskProducers/CASConfigFileTaskProducer.swift diff --git a/Sources/SWBTaskConstruction/CMakeLists.txt b/Sources/SWBTaskConstruction/CMakeLists.txt index 3a32f552..a6dbac60 100644 --- a/Sources/SWBTaskConstruction/CMakeLists.txt +++ b/Sources/SWBTaskConstruction/CMakeLists.txt @@ -51,6 +51,7 @@ add_library(SWBTaskConstruction TaskProducers/StandardTaskProducer.swift TaskProducers/TaskProducer.swift TaskProducers/TaskProducerExtensionPoint.swift + TaskProducers/WorkspaceTaskProducers/CASConfigFileTaskProducer.swift TaskProducers/WorkspaceTaskProducers/BuildDependencyInfoTaskProducer.swift TaskProducers/WorkspaceTaskProducers/CreateBuildDirectoryTaskProducer.swift TaskProducers/WorkspaceTaskProducers/HeadermapVFSTaskProducer.swift diff --git a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift index d6e50110..5d3cbfa9 100644 --- a/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift +++ b/Sources/SWBTaskConstruction/ProductPlanning/ProductPlanner.swift @@ -82,6 +82,7 @@ private struct WorkspaceProductPlanBuilder { HeadermapVFSTaskProducer(context: globalTaskProducerContext, targetContexts: targetContexts), PCHModuleMapTaskProducer(context: globalTaskProducerContext, targetContexts: targetContexts), BuildDependencyInfoTaskProducer(context: globalTaskProducerContext, targetContexts: targetContexts), + CASConfigFileTaskProducer(context: globalTaskProducerContext, targetContexts: targetContexts), ] + (globalProductPlan.planRequest.buildRequest.enableIndexBuildArena ? [IndexBuildVFSDirectoryRemapTaskProducer(context: globalTaskProducerContext)] : []) for taskProducerExtension in await taskProducerExtensions(globalTaskProducerContext.workspaceContext) { diff --git a/Sources/SWBTaskConstruction/TaskProducers/WorkspaceTaskProducers/CASConfigFileTaskProducer.swift b/Sources/SWBTaskConstruction/TaskProducers/WorkspaceTaskProducers/CASConfigFileTaskProducer.swift new file mode 100644 index 00000000..906b1adb --- /dev/null +++ b/Sources/SWBTaskConstruction/TaskProducers/WorkspaceTaskProducers/CASConfigFileTaskProducer.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation +import SWBProtocol + +final class CASConfigFileTaskProducer: StandardTaskProducer, TaskProducer { + private let targetContexts: [TaskProducerContext] + + init(context globalContext: TaskProducerContext, targetContexts: [TaskProducerContext]) { + self.targetContexts = targetContexts + super.init(globalContext) + } + + func generateTasks() async -> [any SWBCore.PlannedTask] { + var tasks = [any PlannedTask]() + do { + let casConfigFiles = try Dictionary(try await targetContexts.concurrentMap(maximumParallelism: 100) { (targetContext: TaskProducerContext) async throws -> (Path, ByteString)? in + let scope = targetContext.settings.globalScope + + // If compilation caching is not on, then there is no file to write. + // The condition here is more relax than the actual check in the compile task generation + // since it won't hurt if the file is not used. + guard scope.evaluate(BuiltinMacros.CLANG_ENABLE_COMPILE_CACHE) || scope.evaluate(BuiltinMacros.SWIFT_ENABLE_COMPILE_CACHE) else { + return nil + } + + // FIXME: we need consistent CAS configuration across all languages. + if !scope.evaluate(BuiltinMacros.COMPILATION_CACHE_REMOTE_SERVICE_PATH).isEmpty && !scope.evaluate(BuiltinMacros.COMPILATION_CACHE_REMOTE_SUPPORTED_LANGUAGES).isEmpty { + return nil + } + + let casOpts = try CASOptions.create(scope, .compiler(.other(dialectName: "swift"))) + struct CASConfig: Encodable { + let CASPath: String + let PluginPath: String? + } + let content = try JSONEncoder().encode(CASConfig(CASPath: casOpts.casPath.str, PluginPath: casOpts.pluginPath?.str)) + let path = scope.evaluate(BuiltinMacros.TARGET_TEMP_DIR).join(".cas-config") + return (path, ByteString(content)) + }.compactMap { $0 }, uniquingKeysWith: { first, second in + guard first == second else { + throw StubError.error("Unexpected difference in CAS config file.\nPath: \(first.asString)\nContent:\(second.asString)") + } + return first + }) + + for (configFilePath, configFileContent) in casConfigFiles { + await appendGeneratedTasks(&tasks) { delegate in + context.writeFileSpec.constructFileTasks(CommandBuildContext(producer: context, scope: context.settings.globalScope, inputs: [], output: configFilePath), delegate, contents: configFileContent, permissions: nil, preparesForIndexing: true, additionalTaskOrderingOptions: [.immediate]) + } + } + } catch { + self.context.error(error.localizedDescription) + } + return tasks + } +} diff --git a/Tests/SWBBuildSystemTests/ClangCompilationCachingTests.swift b/Tests/SWBBuildSystemTests/ClangCompilationCachingTests.swift index a1678a3b..d6fc6f7b 100644 --- a/Tests/SWBBuildSystemTests/ClangCompilationCachingTests.swift +++ b/Tests/SWBBuildSystemTests/ClangCompilationCachingTests.swift @@ -246,6 +246,13 @@ fileprivate struct ClangCompilationCachingTests: CoreBasedTests { } #expect(try readMetrics("one") == #"{"global":{"clangCacheHits":0,"clangCacheMisses":1,"swiftCacheHits":0,"swiftCacheMisses":0},"tasks":{"CompileC":{"cacheMisses":1,"headerDependenciesNotValidatedTasks":1,"moduleDependenciesNotValidatedTasks":1}}}"#) + let CASConfigPath = tmpDirPath.join("Test/aProject/build/aProject.build/Debug\(runDestination == .macOS ? "": "-" + runDestination.platform)/Library.build/.cas-config") + + #expect(try tester.fs.read(CASConfigPath).asString.contains("\"CASPath\":")) + if usePlugin { + #expect(try tester.fs.read(CASConfigPath).asString.contains("\"PluginPath\":")) + } + // Touch the source file to trigger a new scan. try await tester.fs.updateTimestamp(testWorkspace.sourceRoot.join("aProject/file.c")) diff --git a/Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift b/Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift index ff197e51..173ab100 100644 --- a/Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift +++ b/Tests/SWBBuildSystemTests/SwiftCompilationCachingTests.swift @@ -116,6 +116,8 @@ fileprivate struct SwiftCompilationCachingTests: CoreBasedTests { } #expect(try readMetrics("one").contains("\"swiftCacheHits\":0,\"swiftCacheMisses\":\(numCompile)")) + #expect(try tester.fs.read(tmpDirPath.join("Test/aProject/build/aProject.build/Debug-iphoneos/Application.build/.cas-config")).asString.contains("\"CASPath\":")) + // touch a file, clean build folder, and rebuild. try await tester.fs.updateTimestamp(testWorkspace.sourceRoot.join("aProject/App.swift")) try await tester.checkBuild(runDestination: .anyiOSDevice, buildCommand: .cleanBuildFolder(style: .regular), body: { _ in })