File: WorkspaceSettingsCache.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (173 lines) | stat: -rw-r--r-- 10,696 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
//===----------------------------------------------------------------------===//
//
// 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

final class WorkspaceSettingsCache: Sendable {
    unowned private let workspaceContext: WorkspaceContext
    private let macroConfigFileLoader: MacroConfigFileLoader

    init(workspaceContext: WorkspaceContext, macroConfigFileLoader: MacroConfigFileLoader) {
        self.workspaceContext = workspaceContext
        self.macroConfigFileLoader = macroConfigFileLoader
    }

    struct MacroConfigCacheKey: Equatable, Hashable {
        /// The path of the macro config file being loaded.
        let path: Path

        /// The ordered list of search paths in which to look for other macro config files being included by the macro config file being loaded.
        let searchPaths: [Path]
    }

    struct SettingsCacheKey: Equatable, Hashable {
        /// The parameter these settings are for.
        let parameters: BuildParameters

        /// The project GUID the settings are for.
        let projectGUID: String?

        /// The target GUID the settings are for, if any.
        let targetGUID: String?

        /// The purpose of the settings.
        let purpose: SettingsPurpose

        /// The provisioning task inputs contributing to the settings.
        let provisioningTaskInputs: ProvisioningTaskInputs?

        /// Additional properties imparted by dependencies.
        let impartedBuildProperties: [ImpartedBuildProperties]?

        // Using just this instead of all of `impartedBuildProperties` for equality should be fine, because we should only be seeing the same
        // `impartedBuildProperties` each time when looking up cached settings.
        private var impartedMacroDeclarations: [[MacroDeclaration]]? {
            return impartedBuildProperties?.map { return Array($0.buildSettings.valueAssignments.keys) }
        }

        static func == (lhs: SettingsCacheKey, rhs: SettingsCacheKey) -> Bool {
            return lhs.parameters == rhs.parameters && lhs.projectGUID == rhs.projectGUID && lhs.targetGUID == rhs.targetGUID && lhs.purpose == rhs.purpose && lhs.provisioningTaskInputs == rhs.provisioningTaskInputs && lhs.impartedMacroDeclarations == rhs.impartedMacroDeclarations
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(parameters)
            hasher.combine(projectGUID)
            hasher.combine(targetGUID)
            hasher.combine(provisioningTaskInputs)
            hasher.combine(purpose)
            hasher.combine(impartedMacroDeclarations)
        }
    }

    /// Get the cached settings for the given parameters, without considering the context of any project/target.
    public func getCachedSettings(_ parameters: BuildParameters, buildRequestContext: BuildRequestContext, purpose: SettingsPurpose = .build, filesSignature: ([Path]) -> FilesSignature) -> Settings {
        let key = SettingsCacheKey(parameters: parameters, projectGUID: nil, targetGUID: nil, purpose: purpose, provisioningTaskInputs: nil, impartedBuildProperties: nil)

        // Check if there were any changes in used xcconfigs
        return settingsCache.getOrInsert(key, isValid: { settings in filesSignature(settings.macroConfigPaths) == settings.macroConfigSignature }) {
            let settingsContext = SettingsContext(.build, project: nil, target: nil)
            return Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, settingsContext: settingsContext, purpose: purpose, provisioningTaskInputs: nil, impartedBuildProperties: nil)
        }
    }

    /// Get the cached settings for the given parameters and project.
    public func getCachedSettings(_ parameters: BuildParameters, project: Project, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, buildRequestContext: BuildRequestContext, filesSignature: ([Path]) -> FilesSignature) -> Settings {
        return getCachedSettings(parameters, project: project, target: nil, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, buildRequestContext: buildRequestContext, filesSignature: filesSignature)
    }

    /// Get the cached settings for the given parameters and target.
    public func getCachedSettings(_ parameters: BuildParameters, target: Target, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, buildRequestContext: BuildRequestContext, filesSignature: ([Path]) -> FilesSignature) -> Settings {
        return getCachedSettings(parameters, project: workspaceContext.workspace.project(for: target), target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties, buildRequestContext: buildRequestContext, filesSignature: filesSignature)
    }

    /// Private method to get the cached settings for the given parameters, project, and target.
    ///
    /// - remark: This is internal so that clients don't somehow call this with a project which doesn't match the target, except for `BuildRequestContext` which has a cover method for it.  There are public methods covering this one.
    internal func getCachedSettings(_ parameters: BuildParameters, project: Project, target: Target? = nil, purpose: SettingsPurpose = .build, provisioningTaskInputs: ProvisioningTaskInputs? = nil, impartedBuildProperties: [ImpartedBuildProperties]? = nil, buildRequestContext: BuildRequestContext, filesSignature: ([Path]) -> FilesSignature) -> Settings {
        let key = SettingsCacheKey(parameters: parameters, projectGUID: project.guid, targetGUID: target?.guid, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties)

        // Check if there were any changes in used xcconfigs
        return settingsCache.getOrInsert(key, isValid: { settings in filesSignature(settings.macroConfigPaths) == settings.macroConfigSignature }) {
            Settings(workspaceContext: workspaceContext, buildRequestContext: buildRequestContext, parameters: parameters, project: project, target: target, purpose: purpose, provisioningTaskInputs: provisioningTaskInputs, impartedBuildProperties: impartedBuildProperties)
        }
    }

    /// We use a `Lazy` as the value type to allow concurrent settings construction while still ensuring we only ever construct the settings for a particular configuration once, e.g. concurrent access to already constructed settings as well as concurrent setting construction.
    private let settingsCache = ScopedKeepAliveCache(HeavyCache<SettingsCacheKey, Lazy<Settings>>(timeToLive: Tuning.workspaceSettingsCacheTTL))

    func keepAlive<R>(_ f: () throws -> R) rethrows -> R {
        try settingsCache.keepAlive(f)
    }

    func keepAlive<R>(_ f: () async throws -> R) async rethrows -> R {
        try await settingsCache.keepAlive(f)
    }

    /// Get the cached parse information for an `xcconfig` file.
    ///
    /// The loaded table will be defined in the `userNamespace` of the workspace.
    func getCachedMacroConfigFile(_ path: Path, project: Project? = nil, context: MacroConfigLoadContext, filesSignature: ([Path]) -> FilesSignature) -> MacroConfigInfo {
        let searchPaths: [Path]
        if let project {
            searchPaths = [project.sourceRoot]
        } else {
            searchPaths = [Path]()
        }

        var info = macroConfigCache.getOrInsert(MacroConfigCacheKey(path: path, searchPaths: searchPaths), isValid: { info in filesSignature(info.dependencyPaths) == info.signature }) {
            macroConfigFileLoader.loadSettingsFromConfig(path: path, namespace: workspaceContext.workspace.userNamespace, searchPaths: searchPaths, filesSignature: filesSignature)
        }

        // If we failed to read the file, add a diagnostic. We do this outside `loadSettingsFromConfig` because we intentionally avoid passing the Xcode project path there to avoid polluting the cache key (two Xcode projects in the same directory which attempt to load the same config file perform idempotent work, but we want distinct error messages for each access attempt).
        if info.isFileReadFailure {
            let message: String
            switch context {
            case .commandLineConfiguration:
                message = "Unable to open file '\(path.str)' referenced by xcodebuild -xcconfig flag or OverridingXCConfigPath user default."
            case .environmentConfiguration:
                message = "Unable to open file '\(path.str)' referenced by XCODE_XCCONFIG_FILE environment variable."
            case .baseConfiguration:
                message = "Unable to open base configuration reference file '\(path.str)'."
            }
            info.diagnostics.append(Diagnostic(behavior: .error, location: project.map { project in .path(project.xcodeprojPath) } ?? .unknown, data: DiagnosticData(message)))
        }

        return info
    }

    private let macroConfigCache = HeavyCache<MacroConfigCacheKey, Lazy<MacroConfigInfo>>(timeToLive: Tuning.workspaceSettingsCacheTTL)

    // MARK: Stacked search paths caching

    struct StackedSearchPathsCacheKey: Equatable, Hashable {
        let context: String
        let platformIdentifier: String?
        let toolchainIdentifiers: [String]
    }

    public func getCachedStackedSearchPath(context: String, platform: Platform?, toolchains: [Toolchain], _ create: (_ platform: Platform?, _ toolchains: [Toolchain]) -> StackedSearchPath) -> StackedSearchPath {
        stackedSearchPathsRegistry.getOrInsert(.init(context: context, platform: platform, toolchains: toolchains)) {
            create(platform, toolchains)
        }
    }

    private let stackedSearchPathsRegistry = Registry<StackedSearchPathsCacheKey, StackedSearchPath>()
}

extension WorkspaceSettingsCache.StackedSearchPathsCacheKey {
    init(context: String, platform: Platform?, toolchains: [Toolchain]) {
        self.context = context
        self.platformIdentifier = platform?.identifier
        self.toolchainIdentifiers = toolchains.map(\.identifier)
    }
}