diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetGraphReport.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetGraphReport.swift index 6102404a..d741f99b 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetGraphReport.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetGraphReport.swift @@ -43,6 +43,7 @@ struct BazelTargetGraphReport: Codable, Equatable { let platform: String let minimumOsVersion: String let cpuArch: String + let sdkName: String } let topLevelTargets: [TopLevelTarget] diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetPlatformInfo.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetPlatformInfo.swift index a39c0f71..65c59ce0 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetPlatformInfo.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetPlatformInfo.swift @@ -25,12 +25,7 @@ import Foundation struct BazelTargetPlatformInfo { let label: String let topLevelParentLabel: String - let topLevelParentRuleType: TopLevelRuleType let topLevelParentConfig: BazelTargetConfigurationInfo - - var platformSdkName: String { - topLevelParentRuleType.sdkName - } } // Information about a target's Bazel configuration, used to determine @@ -48,4 +43,5 @@ struct BazelTargetConfigurationInfo: Hashable { let minimumOsVersion: String let platform: String let cpuArch: String + let sdkName: String } diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerier.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerier.swift index a42086f2..dedb5b00 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerier.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerier.swift @@ -155,7 +155,7 @@ final class BazelTargetQuerier { /// Runs an aquery across the codebase over a list of specific target dependencies. func aquery( - topLevelTargets: [(String, TopLevelRuleType)], + topLevelTargets: [(String, TopLevelRuleType, UInt32)], config: InitializedServerConfig, mnemonics: [String] ) throws -> ProcessedAqueryResult { diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerierParser.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerierParser.swift index 67fa542c..96c83d00 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerierParser.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetQuerierParser.swift @@ -31,6 +31,7 @@ enum BazelTargetQuerierParserError: Error, LocalizedError { case parentActionNotFound(String, UInt32) case multipleParentActions(String) case configurationNotFound(UInt32) + case sdkNameNotFound(String) case indexOutOfBounds(Int, Int) case unexpectedLanguageRule(String, String) case unexpectedTargetType(Int) @@ -47,9 +48,11 @@ enum BazelTargetQuerierParserError: Error, LocalizedError { return "Parent action \(id) for parent \(parent) not found in the aquery output." case .multipleParentActions(let parent): return - "Multiple parent actions found for \(parent). This means your project is somehow building multiple variants of the same top-level target, which the BSP cannot handle at the moment. This can happen for example if you are building for multiple platforms." + "Multiple parent actions found for \(parent) in the aquery output. This means your project is building multiple variants of the same top-level target, which the BSP cannot handle today." case .configurationNotFound(let id): return "Configuration \(id) not found in the aquery output." + case .sdkNameNotFound(let label): + return "SDK info not found for \(label) in the aquery output." case .indexOutOfBounds(let index, let line): return "Index \(index) is out of bounds for array at line \(line)" case .unexpectedLanguageRule(let target, let ruleClass): @@ -80,7 +83,7 @@ protocol BazelTargetQuerierParser: AnyObject { func processAquery( from data: Data, - topLevelTargets: [(String, TopLevelRuleType)], + topLevelTargets: [(String, TopLevelRuleType, UInt32)], ) throws -> ProcessedAqueryResult } @@ -105,9 +108,9 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { // Separate / categorize all the data we received from the cquery. let supportedTopLevelRuleTypesSet = Set(supportedTopLevelRuleTypes) let supportedTestBundleRulesSet = Set(testBundleRules) - var topLevelTargets: [(BlazeQuery_Target, TopLevelRuleType)] = [] + var topLevelLabelToConfigMap: [String: UInt32] = [:] var configurationToTopLevelLabelsMap: [UInt32: [String]] = [:] - var bazelLabelToParentConfigMap: [String: UInt32] = [:] + var allTopLevelLabels = [(String, TopLevelRuleType)]() var allAliases = [BlazeQuery_Target]() var allTestBundles = [BlazeQuery_Target]() var unfilteredDependencyTargets = [Analysis_ConfiguredTarget]() @@ -120,12 +123,13 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { let kind = target.rule.ruleClass if let topLevelRuleType = TopLevelRuleType(rawValue: kind) { if supportedTopLevelRuleTypesSet.contains(topLevelRuleType) { - topLevelTargets.append((target, topLevelRuleType)) + let label = target.rule.name + allTopLevelLabels.append((label, topLevelRuleType)) // If this rule generates a bundle target, the real information we're looking for will be available // on said bundle target and will be handled below. if topLevelRuleType.testBundleRule == nil { - configurationToTopLevelLabelsMap[configuration, default: []].append(target.rule.name) - bazelLabelToParentConfigMap[target.rule.name] = configuration + configurationToTopLevelLabelsMap[configuration, default: []].append(label) + topLevelLabelToConfigMap[label] = configuration } } } else if kind == "alias" { @@ -141,7 +145,7 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { target.rule.name.dropLast(TopLevelRuleType.testBundleRuleSuffix.count) ) configurationToTopLevelLabelsMap[configuration, default: []].append(realTopLevelName) - bazelLabelToParentConfigMap[realTopLevelName] = configuration + topLevelLabelToConfigMap[realTopLevelName] = configuration } else { unfilteredDependencyTargets.append(configuredTarget) } @@ -159,6 +163,16 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { } } + // We can now properly connect each top-level label to its configuration id. + var topLevelTargets: [(String, TopLevelRuleType, UInt32)] = [] + for (label, ruleType) in allTopLevelLabels { + guard let configId = topLevelLabelToConfigMap[label] else { + logger.error("Missing info for \(label) in topLevelLabelToConfigMap. This should not happen.") + continue + } + topLevelTargets.append((label, ruleType, configId)) + } + guard !topLevelTargets.isEmpty else { throw BazelTargetQuerierParserError.noTopLevelTargets(supportedTopLevelRuleTypes) } @@ -170,7 +184,7 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { logger.logFullObjectInMultipleLogMessages( level: .info, header: "Top-level targets", - String(topLevelTargets.map { $0.0.rule.name }.joined(separator: ", ")) + String(topLevelTargets.map { $0.0 }.joined(separator: ", ")) ) // The cquery will contain data about bundled apps (e.g extensions, companion watchOS apps) regardless of our filters. @@ -179,8 +193,8 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { // matches what we've parsed above as a valid top-level target. // We also use this opportunity to match which targets belong to which top-level targets. let supportedDependencyRuleTypesSet = Set(supportedDependencyRuleTypes) - var dependencyTargets: [(BlazeQuery_Target, DependencyRuleType)] = [] - var seenDependencyLabels = Set() + var dependencyTargets: [(BlazeQuery_Target, DependencyRuleType, BuildTargetIdentifier, UInt32)] = [] + var bspUriToParentConfigMap: [URI: UInt32] = [:] for configuredTarget in unfilteredDependencyTargets { let configuration = configuredTarget.configurationID guard configurationToTopLevelLabelsMap[configuration] != nil else { @@ -192,19 +206,14 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { else { continue } - guard !seenDependencyLabels.contains(label) else { - // FIXME: It should be possible to lift this limitation, I just didn't check deep enough how to structure it. - // We should notify sourcekit-lsp of all different target variants. - // Note: When fixing this, the aquery logic below also needs to be updated to handle multiple variants. - // Same for the logic in platformBuildLabelInfo. - logger.debug( - "Skipping duplicate entry for dependency \(label, privacy: .public). This can happen if your configuration contains multiple variants of the same target and should be fine as long as the inputs are the same across all variants." - ) - continue - } - bazelLabelToParentConfigMap[label] = configuration - seenDependencyLabels.insert(label) - dependencyTargets.append((configuredTarget.target, ruleType)) + let id = try label.toTargetId( + rootUri: rootUri, + workspaceName: workspaceName, + executionRoot: executionRoot, + config: configuration + ) + bspUriToParentConfigMap[id] = configuration + dependencyTargets.append((configuredTarget.target, ruleType, BuildTargetIdentifier(uri: id), configuration)) } logger.debug("Parsed \(dependencyTargets.count, privacy: .public) dependency targets") @@ -221,17 +230,10 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { // Do the same for the dependencies list. // This also defines which dependencies are "valid", // because the cquery result's deps field ignores the filters applied to the query. - var depLabelToUriMap: [String: BuildTargetIdentifier] = [:] - for (target, _) in dependencyTargets { + var depLabelToUriMap: [String: [(BuildTargetIdentifier, UInt32)]] = [:] + for (target, _, id, config) in dependencyTargets { let label = target.rule.name - depLabelToUriMap[label] = - (BuildTargetIdentifier( - uri: try label.toTargetId( - rootUri: rootUri, - workspaceName: workspaceName, - executionRoot: executionRoot - ) - )) + depLabelToUriMap[label, default: []].append((id, config)) } // Similarly, process the list of aliases. The cquery result's deps field does not @@ -263,24 +265,13 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { guard depLabelToUriMap.keys.contains(realLabel) else { continue } - depLabelToUriMap[label] = - (BuildTargetIdentifier( - uri: try realLabel.toTargetId( - rootUri: rootUri, - workspaceName: workspaceName, - executionRoot: executionRoot - ) - )) + depLabelToUriMap[label] = depLabelToUriMap[realLabel] } - let buildTargets: [(BuildTarget, SourcesItem)] = try dependencyTargets.map { (target, ruleType) in + let buildTargets: [(BuildTarget, SourcesItem)] = try dependencyTargets.map { (target, ruleType, id, config) in let rule = target.rule - let idUri: URI = try rule.name.toTargetId( - rootUri: rootUri, - workspaceName: workspaceName, - executionRoot: executionRoot - ) - let id = BuildTargetIdentifier(uri: idUri) + let idUri = id.uri + let baseDirectory: URI? = idUri.toBaseDirectory() let sourcesItem = try processSrcsAttr( @@ -293,7 +284,8 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { let deps = try processDependenciesAttr( rule: rule, isBuildTestRule: false, - depLabelToUriMap: depLabelToUriMap + depLabelToUriMap: depLabelToUriMap, + config: config ) // AFAIK these settings serve no particular purpose today and are ignored by sourcekit-lsp. @@ -332,7 +324,6 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { var bspURIsToBazelLabelsMap: [URI: String] = [:] var bspURIsToSrcsMap: [URI: SourcesItem] = [:] var srcToBspURIsMap: [URI: [URI]] = [:] - var topLevelLabelToRuleMap: [String: TopLevelRuleType] = [:] for dependencyTargetInfo in buildTargets { let target = dependencyTargetInfo.0 let sourcesItem = dependencyTargetInfo.1 @@ -347,20 +338,15 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { srcToBspURIsMap[src.uri, default: []].append(uri) } } - for (target, ruleType) in topLevelTargets { - let label = target.rule.name - topLevelLabelToRuleMap[label] = ruleType - } return ProcessedCqueryResult( buildTargets: buildTargets.map { $0.0 }, - topLevelTargets: topLevelTargets.map { ($0.0.rule.name, $0.1) }, + topLevelTargets: topLevelTargets, bspURIsToBazelLabelsMap: bspURIsToBazelLabelsMap, bspURIsToSrcsMap: bspURIsToSrcsMap, srcToBspURIsMap: srcToBspURIsMap, - topLevelLabelToRuleMap: topLevelLabelToRuleMap, configurationToTopLevelLabelsMap: configurationToTopLevelLabelsMap, - bazelLabelToParentConfigMap: bazelLabelToParentConfigMap + bspUriToParentConfigMap: bspUriToParentConfigMap ) } @@ -474,16 +460,24 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { private func processDependenciesAttr( rule: BlazeQuery_Rule, isBuildTestRule: Bool, - depLabelToUriMap: [String: BuildTargetIdentifier], + depLabelToUriMap: [String: [(BuildTargetIdentifier, UInt32)]], + config: UInt32 ) throws -> [BuildTargetIdentifier] { let attrName = isBuildTestRule ? "targets" : "deps" let depsAttribute = rule.attribute.first { $0.name == attrName }?.stringListValue ?? [] let implDeps = rule.attribute.first { $0.name == "implementation_deps" }?.stringListValue ?? [] return (depsAttribute + implDeps).compactMap { label in - guard let depUri = depLabelToUriMap[label] else { + guard let depUris = depLabelToUriMap[label] else { return nil } - return depUri + // When a dependency is available over multiple configs, we need to find the one matching the parent. + guard let depUri = depUris.first(where: { $0.1 == config }) else { + logger.info( + "No dependency found for \(label) with config \(config). Falling back to first available config." + ) + return depUris.first?.0 + } + return depUri.0 } } } @@ -493,7 +487,7 @@ final class BazelTargetQuerierParserImpl: BazelTargetQuerierParser { extension BazelTargetQuerierParserImpl { func processAquery( from data: Data, - topLevelTargets: [(String, TopLevelRuleType)], + topLevelTargets: [(String, TopLevelRuleType, UInt32)] ) throws -> ProcessedAqueryResult { let aquery = try BazelProtobufBindings.parseActionGraph(data: data) @@ -524,8 +518,11 @@ extension BazelTargetQuerierParserImpl { } // Now, locate the Bazel config information for each of our top-level targets. - var topLevelLabelToConfigMap: [String: BazelTargetConfigurationInfo] = [:] - for (target, ruleType) in topLevelTargets { + var topLevelConfigIdToInfoMap: [UInt32: BazelTargetConfigurationInfo] = [:] + for (target, ruleType, configId) in topLevelTargets { + guard topLevelConfigIdToInfoMap[configId] == nil else { + continue + } let configInfo = try topLevelConfigInfo( ofTarget: target, withType: ruleType, @@ -533,14 +530,14 @@ extension BazelTargetQuerierParserImpl { aqueryActions: actions, aqueryConfigurations: configurations ) - topLevelLabelToConfigMap[target] = configInfo + topLevelConfigIdToInfoMap[configId] = configInfo } return ProcessedAqueryResult( targets: targets, actions: actions, configurations: configurations, - topLevelLabelToConfigMap: topLevelLabelToConfigMap + topLevelConfigIdToInfoMap: topLevelConfigIdToInfoMap ) } @@ -576,6 +573,10 @@ extension BazelTargetQuerierParserImpl { guard let fullConfig = aqueryConfigurations[configId]?.mnemonic else { throw BazelTargetQuerierParserError.configurationNotFound(configId) } + guard let sdkName = parentAction.environmentVariables.first(where: { $0.key == "APPLE_SDK_PLATFORM" })?.value + else { + throw BazelTargetQuerierParserError.sdkNameNotFound(effectiveParentLabel) + } let configComponents = fullConfig.components(separatedBy: "-") // min15.0 -> 15.0 let minTargetArg = String(try configComponents.getIndexThrowing(4).dropFirst(3)) @@ -601,6 +602,7 @@ extension BazelTargetQuerierParserImpl { minimumOsVersion: minTargetArg, platform: String(platform), cpuArch: String(cpuArch), + sdkName: sdkName.lowercased() ) } } @@ -622,15 +624,22 @@ extension String { /// For local labels: file:///___ /// For external labels: file:///external//___ /// - fileprivate func toTargetId(rootUri: String, workspaceName: String, executionRoot: String) throws -> URI { + fileprivate func toTargetId( + rootUri: String, + workspaceName: String, + executionRoot: String, + config: UInt32 + ) throws -> URI { let (repoName, packageName, targetName) = try splitTargetLabel(workspaceName: workspaceName) let packagePath = packageName.isEmpty ? "" : "/" + packageName let path: String if repoName == workspaceName { - path = "file://" + rootUri + packagePath + "/" + targetName + path = "file://" + rootUri + packagePath + "/" + targetName + "_" + String(config) } else { // External repo: use execution root + external path - path = "file://" + executionRoot + "/external/" + repoName + packagePath + "/" + targetName + path = + "file://" + executionRoot + "/external/" + repoName + packagePath + "/" + targetName + "_" + + String(config) } guard let uri = try? URI(string: path) else { throw BazelTargetQuerierParserError.convertUriFailed(path) diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetStore.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetStore.swift index aea9b4b5..46d60873 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetStore.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/BazelTargetStore.swift @@ -43,10 +43,10 @@ protocol BazelTargetStore: AnyObject { enum BazelTargetStoreError: Error, LocalizedError { case unknownBSPURI(URI) case unableToMapBazelLabelToParents(String) - case unableToMapBazelLabelToTopLevelRuleType(String) - case unableToMapBazelLabelToTopLevelConfig(String) - case unableToMapBazelLabelToParentConfig(String) - case unableToMapConfigToTopLevelLabels(UInt32) + case unableToMapConfigIdToTopLevelConfig(UInt32) + case unableToMapBSPURIToParentConfig(URI) + case unableToMapConfigIdToTopLevelLabels(UInt32) + case unableToMapTopLevelLabelToConfig(String) case noCachedAquery var errorDescription: String? { @@ -55,14 +55,14 @@ enum BazelTargetStoreError: Error, LocalizedError { return "Unable to map '\(uri)' to a Bazel target label" case .unableToMapBazelLabelToParents(let label): return "Unable to map '\(label)' to its parents" - case .unableToMapBazelLabelToTopLevelRuleType(let label): - return "Unable to map '\(label)' to its top-level rule type" - case .unableToMapBazelLabelToTopLevelConfig(let label): - return "Unable to map '\(label)' to its top-level configuration" - case .unableToMapBazelLabelToParentConfig(let label): - return "Unable to map '\(label)' to its parent configuration" - case .unableToMapConfigToTopLevelLabels(let config): - return "Unable to map configuration '\(config)' to its top-level labels" + case .unableToMapConfigIdToTopLevelConfig(let config): + return "Unable to map configId '\(config)' to its top-level configuration" + case .unableToMapBSPURIToParentConfig(let uri): + return "Unable to map '\(uri)' to its parent configuration" + case .unableToMapConfigIdToTopLevelLabels(let config): + return "Unable to map configId '\(config)' to its top-level labels" + case .unableToMapTopLevelLabelToConfig(let label): + return "Unable to map top-level label '\(label)' to its configuration" case .noCachedAquery: return "No cached aquery result found in the store." } @@ -132,26 +132,18 @@ final class BazelTargetStoreImpl: BazelTargetStore, @unchecked Sendable { return bspURIs } - /// Retrieves the top-level rule type for a given Bazel **top-level** target label. - func topLevelRuleType(forBazelLabel label: String) throws -> TopLevelRuleType { - guard let ruleType = cqueryResult?.topLevelLabelToRuleMap[label] else { - throw BazelTargetStoreError.unableToMapBazelLabelToTopLevelRuleType(label) - } - return ruleType - } - /// Retrieves the configuration information for a given Bazel **top-level** target label. - func topLevelConfigInfo(forBazelLabel label: String) throws -> BazelTargetConfigurationInfo { - guard let config = aqueryResult?.topLevelLabelToConfigMap[label] else { - throw BazelTargetStoreError.unableToMapBazelLabelToTopLevelConfig(label) + func topLevelConfigInfo(forConfig configId: UInt32) throws -> BazelTargetConfigurationInfo { + guard let config = aqueryResult?.topLevelConfigIdToInfoMap[configId] else { + throw BazelTargetStoreError.unableToMapConfigIdToTopLevelConfig(configId) } return config } - /// Retrieves the configuration for a given Bazel target label. - func parentConfig(forBazelLabel label: String) throws -> UInt32 { - guard let config = cqueryResult?.bazelLabelToParentConfigMap[label] else { - throw BazelTargetStoreError.unableToMapBazelLabelToParentConfig(label) + /// Retrieves the available configurations for a given Bazel target label. + func parentConfig(forBSPURI uri: URI) throws -> UInt32 { + guard let config = cqueryResult?.bspUriToParentConfigMap[uri] else { + throw BazelTargetStoreError.unableToMapBSPURIToParentConfig(uri) } return config } @@ -159,7 +151,7 @@ final class BazelTargetStoreImpl: BazelTargetStore, @unchecked Sendable { /// Retrieves the list of top-level labels for a given configuration. func topLevelLabels(forConfig config: UInt32) throws -> [String] { guard let labels = cqueryResult?.configurationToTopLevelLabelsMap[config] else { - throw BazelTargetStoreError.unableToMapConfigToTopLevelLabels(config) + throw BazelTargetStoreError.unableToMapConfigIdToTopLevelLabels(config) } return labels } @@ -168,19 +160,14 @@ final class BazelTargetStoreImpl: BazelTargetStore, @unchecked Sendable { /// This is used to determine the correct set of compiler flags for the target / platform combo. func platformBuildLabelInfo(forBSPURI uri: URI) throws -> BazelTargetPlatformInfo { let bazelLabel = try bazelTargetLabel(forBSPURI: uri) - let configId = try parentConfig(forBazelLabel: bazelLabel) + let configId = try parentConfig(forBSPURI: uri) + let config = try topLevelConfigInfo(forConfig: configId) let parents = try topLevelLabels(forConfig: configId) - // FIXME: When a target can compile to multiple platforms, the way Xcode handles it is by selecting - // the one matching your selected simulator in the IDE. We don't have any sort of special IDE integration - // at the moment, so for now we just select the first parent. - // We are also not processing the different variants at all (see FIXME in BazelTargetQuerierParser.swift). + // Since the config of these parents are all the same, it doesn't matter which one we pick here. let parentToUse = parents[0] - let rule = try topLevelRuleType(forBazelLabel: parentToUse) - let config = try topLevelConfigInfo(forBazelLabel: parentToUse) return BazelTargetPlatformInfo( label: bazelLabel, topLevelParentLabel: parentToUse, - topLevelParentRuleType: rule, topLevelParentConfig: config ) } @@ -271,9 +258,8 @@ extension BazelTargetStoreImpl { var reportTopLevel: [BazelTargetGraphReport.TopLevelTarget] = [] var reportConfigurations: [UInt32: BazelTargetGraphReport.Configuration] = [:] let topLevelTargets = cqueryResult?.topLevelTargets ?? [] - for (label, ruleType) in topLevelTargets { - let topLevelConfig = try topLevelConfigInfo(forBazelLabel: label) - let configId = try parentConfig(forBazelLabel: label) + for (label, ruleType, configId) in topLevelTargets { + let topLevelConfig = try topLevelConfigInfo(forConfig: configId) reportTopLevel.append( .init( label: label, @@ -286,7 +272,8 @@ extension BazelTargetStoreImpl { id: configId, platform: topLevelConfig.platform, minimumOsVersion: topLevelConfig.minimumOsVersion, - cpuArch: topLevelConfig.cpuArch + cpuArch: topLevelConfig.cpuArch, + sdkName: topLevelConfig.sdkName ) ) } @@ -294,7 +281,7 @@ extension BazelTargetStoreImpl { let dependencyTargets = cqueryResult?.buildTargets ?? [] for target in dependencyTargets { guard let label = target.displayName else { continue } - let configId = try parentConfig(forBazelLabel: label) + let configId = try parentConfig(forBSPURI: target.id.uri) reportDependencies.append(.init(label: label, configId: configId)) } return BazelTargetGraphReport( diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedAqueryResult.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedAqueryResult.swift index 3eaae0a9..768f034e 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedAqueryResult.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedAqueryResult.swift @@ -28,5 +28,5 @@ struct ProcessedAqueryResult: Hashable { let targets: [String: Analysis_Target] let actions: [UInt32: [Analysis_Action]] let configurations: [UInt32: Analysis_Configuration] - let topLevelLabelToConfigMap: [String: BazelTargetConfigurationInfo] + let topLevelConfigIdToInfoMap: [UInt32: BazelTargetConfigurationInfo] } diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedCqueryResult.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedCqueryResult.swift index 77f03ab4..c4da2c90 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedCqueryResult.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/ProcessedCqueryResult.swift @@ -25,11 +25,10 @@ private let logger = makeFileLevelBSPLogger() struct ProcessedCqueryResult { let buildTargets: [BuildTarget] - let topLevelTargets: [(String, TopLevelRuleType)] + let topLevelTargets: [(String, TopLevelRuleType, UInt32)] let bspURIsToBazelLabelsMap: [URI: String] let bspURIsToSrcsMap: [URI: SourcesItem] let srcToBspURIsMap: [URI: [URI]] - let topLevelLabelToRuleMap: [String: TopLevelRuleType] let configurationToTopLevelLabelsMap: [UInt32: [String]] - let bazelLabelToParentConfigMap: [String: UInt32] + let bspUriToParentConfigMap: [URI: UInt32] } diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/TopLevelRuleType.swift b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/TopLevelRuleType.swift index 84cc660c..ba269922 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/TopLevelRuleType.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/BuildTargets/TopLevelRuleType.swift @@ -99,38 +99,4 @@ public enum TopLevelRuleType: String, CaseIterable, ExpressibleByArgument, Senda default: return false } } - - // FIXME: Not the best way to handle this as we need to eventually - // handle device builds as well - var sdkName: String { - switch self { - case .iosApplication: return "iphonesimulator" - case .iosAppClip: return "iphonesimulator" - case .iosExtension: return "iphonesimulator" - case .iosUnitTest: return "iphonesimulator" - case .iosUiTest: return "iphonesimulator" - case .iosBuildTest: return "iphonesimulator" - case .watchosApplication: return "watchsimulator" - case .watchosExtension: return "watchsimulator" - case .watchosUnitTest: return "watchsimulator" - case .watchosUiTest: return "watchsimulator" - case .watchosBuildTest: return "watchsimulator" - case .macosApplication: return "macosx" - case .macosExtension: return "macosx" - case .macosCommandLineApplication: return "macosx" - case .macosUnitTest: return "macosx" - case .macosUiTest: return "macosx" - case .macosBuildTest: return "macosx" - case .tvosApplication: return "appletvsimulator" - case .tvosExtension: return "appletvsimulator" - case .tvosUnitTest: return "appletvsimulator" - case .tvosUiTest: return "appletvsimulator" - case .tvosBuildTest: return "appletvsimulator" - case .visionosApplication: return "xrsimulator" - case .visionosExtension: return "xrsimulator" - case .visionosUnitTest: return "xrsimulator" - case .visionosUiTest: return "xrsimulator" - case .visionosBuildTest: return "xrsimulator" - } - } } diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift b/Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift index 399c74d4..e48ccda3 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/InitializeHandler.swift @@ -154,7 +154,17 @@ final class InitializeHandler { } func getSDKRootPaths(with commandRunner: CommandRunner) -> [String: String] { - let supportedSDKTypes = Set(baseConfig.topLevelRulesToDiscover.map { $0.sdkName }).sorted() + let supportedSDKTypes = [ + "appletvsimulator", + "appletvos", + "iphonesimulator", + "iphoneos", + "macosx", + "watchsimulator", + "watchos", + "xrsimulator", + "xros", + ] let sdkRootPaths: [String: String] = supportedSDKTypes.reduce(into: [:]) { result, sdkType in // This will fail if the user doesn't have the SDK installed, which is fine. guard let sdkRootPath: String? = try? commandRunner.run("xcrun --sdk \(sdkType) --show-sdk-path") else { diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/PrepareHandler.swift b/Sources/SourceKitBazelBSP/RequestHandlers/PrepareHandler.swift index 637038a8..0ea572fb 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/PrepareHandler.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/PrepareHandler.swift @@ -68,7 +68,7 @@ final class PrepareHandler { ) { let targetsToBuild = request.targets guard !targetsToBuild.isEmpty else { - logger.info("No targets to build.") + logger.debug("No targets to build.") reply(.success(VoidResponse())) return } @@ -143,9 +143,9 @@ final class PrepareHandler { completion: @escaping ((ResponseError?) -> Void) ) throws { if labelsToBuild.count == 1 { - logger.info("Will build \(labelsToBuild[0].joined(separator: ", "), privacy: .public)") + logger.debug("Will build \(labelsToBuild[0].joined(separator: ", "), privacy: .public)") } else { - logger.info( + logger.debug( "Will build \(labelsToBuild.flatMap { $0 }.joined(separator: ", "), privacy: .public) over \(labelsToBuild.count, privacy: .public) invocations" ) } @@ -175,11 +175,11 @@ final class PrepareHandler { ) process.setTerminationHandler { code, stderr in if code == 0 { - logger.info("Finished building! (Request ID: \(id.description, privacy: .public))") + logger.debug("Finished building! (Request ID: \(id.description, privacy: .public))") completion(nil) } else { if code == 8 { - logger.info("Build (Request ID: \(id.description, privacy: .public)) was cancelled.") + logger.debug("Build (Request ID: \(id.description, privacy: .public)) was cancelled.") completion(ResponseError.cancelled) } else { logger.logFullObjectInMultipleLogMessages( diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift index 835d3ba0..c2583d79 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift @@ -127,7 +127,7 @@ final class BazelTargetCompilerArgsExtractor { return [] } - logger.info( + logger.debug( "Fetching SKOptions for \(platformInfo.label), strategy: \(strategy)", ) @@ -137,14 +137,14 @@ final class BazelTargetCompilerArgsExtractor { strategy: strategy ) - logger.info("Fetching compiler args for \(cacheKey, privacy: .public)") + logger.debug("Fetching compiler args for \(cacheKey, privacy: .public)") if let cached = argsCache[cacheKey] { logger.debug("Returning cached results") return cached } // First, determine the SDK root based on the platform the target is built for. - let platformSdk = platformInfo.platformSdkName + let platformSdk = platformInfo.topLevelParentConfig.sdkName guard let sdkRoot: String = config.sdkRootPaths[platformSdk] else { throw BazelTargetCompilerArgsExtractorError.sdkRootNotFound(platformSdk) } @@ -165,7 +165,7 @@ final class BazelTargetCompilerArgsExtractor { effectiveConfigName: platformInfo.topLevelParentConfig.effectiveConfigurationName ) - logger.info("Finished processing compiler arguments") + logger.debug("Finished processing compiler arguments") logger.logFullObjectInMultipleLogMessages( level: .debug, header: "Parsed compiler arguments", diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/TargetSourcesHandler.swift b/Sources/SourceKitBazelBSP/RequestHandlers/TargetSourcesHandler.swift index 7f7a4e20..7146e565 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/TargetSourcesHandler.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/TargetSourcesHandler.swift @@ -38,13 +38,13 @@ final class TargetSourcesHandler { _: RequestID ) throws -> BuildTargetSourcesResponse { let targets = request.targets - logger.info("Fetching sources for \(targets.count, privacy: .public) targets") + logger.debug("Fetching sources for \(targets.count, privacy: .public) targets") let srcs: [SourcesItem] = try targetStore.stateLock.withLockUnchecked { try targets.map { try targetStore.bazelTargetSrcs(forBSPURI: $0.uri) } } - logger.info( + logger.debug( "Returning \(srcs.count, privacy: .public) source specs" ) diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/WatchedFiles/WatchedFileChangeHandler.swift b/Sources/SourceKitBazelBSP/RequestHandlers/WatchedFiles/WatchedFileChangeHandler.swift index b37ccdcd..6099c5b1 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/WatchedFiles/WatchedFileChangeHandler.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/WatchedFiles/WatchedFileChangeHandler.swift @@ -58,7 +58,7 @@ final class WatchedFileChangeHandler { } guard !changes.isEmpty else { - logger.info("No (supported) file changes to process.") + logger.debug("No (supported) file changes to process.") return } @@ -75,11 +75,11 @@ final class WatchedFileChangeHandler { // If we received this notification before the build graph was calculated, we should stop. guard targetStore.isInitialized else { - logger.info("Received file changes before the build graph was calculated. Ignoring.") + logger.debug("Received file changes before the build graph was calculated. Ignoring.") return [] } - logger.info("Received \(changes.count, privacy: .public) file changes") + logger.debug("Received \(changes.count, privacy: .public) file changes") let deletedFiles = changes.filter { $0.type == .deleted } let createdFiles = changes.filter { $0.type == .created } diff --git a/Sources/SourceKitBazelBSP/Server/MessageHandler/BSPMessageHandler.swift b/Sources/SourceKitBazelBSP/Server/MessageHandler/BSPMessageHandler.swift index 3c764e4c..71a82f4a 100644 --- a/Sources/SourceKitBazelBSP/Server/MessageHandler/BSPMessageHandler.swift +++ b/Sources/SourceKitBazelBSP/Server/MessageHandler/BSPMessageHandler.swift @@ -60,7 +60,7 @@ final class BSPMessageHandler: MessageHandler { } func handle(_ notification: Notification) { - logger.info("Received notification: \(Notification.method, privacy: .public)") + logger.debug("Received notification: \(Notification.method, privacy: .public)") do { let handler = try getHandler(for: notification, state: state) try handler(notification) @@ -74,13 +74,13 @@ final class BSPMessageHandler: MessageHandler { id: RequestID, reply: @escaping (LSPResult) -> Void ) { - logger.info("Received request: \(Request.method, privacy: .public)") + logger.debug("Received request: \(Request.method, privacy: .public)") do { let handler = try getHandler(for: request, id, reply, state: state) handler(request, id) { [buildLSPError] result in do { let response = try result.get() - logger.info("Replying to \(Request.method, privacy: .public)") + logger.debug("Replying to \(Request.method, privacy: .public)") reply(.success(response)) } catch { logger.error( diff --git a/Sources/SourceKitBazelBSP/SharedUtils/Shell/ShellCommandRunner.swift b/Sources/SourceKitBazelBSP/SharedUtils/Shell/ShellCommandRunner.swift index a037b9e4..70a52be5 100644 --- a/Sources/SourceKitBazelBSP/SharedUtils/Shell/ShellCommandRunner.swift +++ b/Sources/SourceKitBazelBSP/SharedUtils/Shell/ShellCommandRunner.swift @@ -55,7 +55,7 @@ struct ShellCommandRunner: CommandRunner { runningProcess.attachPipes() - logger.info("Running shell: \(cmd, privacy: .public)") + logger.debug("Running shell: \(cmd, privacy: .public)") try process.run() return runningProcess diff --git a/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift b/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift index a2209ba7..f9ec0259 100644 --- a/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift +++ b/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift @@ -32,11 +32,12 @@ struct BazelTargetCompilerArgsExtractorTests { let helloWorldConfig: BazelTargetConfigurationInfo init() throws { + let configId: UInt32 = 1 let aqueryResult = try BazelTargetQuerierParserImpl().processAquery( from: exampleAqueryOutput, - topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication)] + topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication, configId)] ) - self.helloWorldConfig = try #require(aqueryResult.topLevelLabelToConfigMap["//HelloWorld:HelloWorld"]) + self.helloWorldConfig = try #require(aqueryResult.topLevelConfigIdToInfoMap[configId]) self.aqueryResult = aqueryResult } @@ -87,7 +88,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:HelloWorldLib", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .swiftModule, @@ -103,7 +103,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:TodoObjCSupport", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .cImpl("HelloWorld/TodoObjCSupport/Sources/SKObjCUtils.m", "objective-c"), @@ -120,7 +119,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:TodoObjCSupport", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .cImpl("HelloWorld/TodoObjCSupport/Sources/SomethingElse.m", "objective-c"), @@ -140,7 +138,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:TodoObjCSupport", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .cHeader, @@ -232,7 +229,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:SomethingElseLib", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .swiftModule, @@ -253,7 +249,6 @@ struct BazelTargetCompilerArgsExtractorTests { forTarget: BazelTargetPlatformInfo( label: "//HelloWorld:HelloWorldLib", topLevelParentLabel: "//HelloWorld:HelloWorld", - topLevelParentRuleType: .iosApplication, topLevelParentConfig: helloWorldConfig ), withStrategy: .swiftModule, diff --git a/Tests/SourceKitBazelBSPTests/BazelTargetQuerierParserImplTests.swift b/Tests/SourceKitBazelBSPTests/BazelTargetQuerierParserImplTests.swift index 2457fdec..81c508bc 100644 --- a/Tests/SourceKitBazelBSPTests/BazelTargetQuerierParserImplTests.swift +++ b/Tests/SourceKitBazelBSPTests/BazelTargetQuerierParserImplTests.swift @@ -50,126 +50,109 @@ struct BazelTargetQuerierParserImplTests { toolchainPath: Self.mockToolchainPath ) - // Pre-create URIs - let baseDir = try URI(string: "file:///path/to/project/HelloWorld/") - let expandedTemplateUri = try URI(string: "file:///path/to/project/HelloWorld/ExpandedTemplate") - let generatedDummyUri = try URI(string: "file:///path/to/project/HelloWorld/GeneratedDummy") - let helloWorldLibUri = try URI(string: "file:///path/to/project/HelloWorld/HelloWorldLib") - let helloWorldTestsLibUri = try URI(string: "file:///path/to/project/HelloWorld/HelloWorldTestsLib") - let macAppLibUri = try URI(string: "file:///path/to/project/HelloWorld/MacAppLib") - let macAppTestsLibUri = try URI(string: "file:///path/to/project/HelloWorld/MacAppTestsLib") - let macCLIAppLibUri = try URI(string: "file:///path/to/project/HelloWorld/MacCLIAppLib") - let todoModelsUri = try URI(string: "file:///path/to/project/HelloWorld/TodoModels") - let todoObjCSupportUri = try URI(string: "file:///path/to/project/HelloWorld/TodoObjCSupport") - let todoCSupport = try URI(string: "file:///path/to/project/HelloWorld/TodoCSupport") - let watchAppLibUri = try URI(string: "file:///path/to/project/HelloWorld/WatchAppLib") - let watchAppTestsLibUri = try URI(string: "file:///path/to/project/HelloWorld/WatchAppTestsLib") - - let expectedCapabilities = BuildTargetCapabilities( - canCompile: true, - canTest: false, - canRun: false, - canDebug: false - ) - - let toolchainUri = try URI(string: "file://" + Self.mockToolchainPath) - let expectedData = SourceKitBuildTarget(toolchain: toolchainUri).encodeToLSPAny() - - func makeExpectedTarget( - uri: URI, - displayName: String, - language: Language = .swift, - dependencies: [URI] = [] - ) -> BuildTarget { - BuildTarget( - id: BuildTargetIdentifier(uri: uri), - displayName: displayName, - baseDirectory: baseDir, - tags: [.library], - capabilities: expectedCapabilities, - languageIds: [language], - dependencies: dependencies.map { BuildTargetIdentifier(uri: $0) }, - dataKind: .sourceKit, - data: expectedData - ) + // Expected target properties (language and dependency labels) + // Note: With multi-variant support, targets can appear multiple times with different configs. + // URIs now include config IDs, so we verify by display name and check properties. + struct ExpectedTargetInfo { + let displayName: String + let language: Language + let dependencyLabels: Set } - let expectedBuildTargets = [ - makeExpectedTarget(uri: expandedTemplateUri, displayName: "//HelloWorld:ExpandedTemplate"), - makeExpectedTarget(uri: generatedDummyUri, displayName: "//HelloWorld:GeneratedDummy"), - makeExpectedTarget( - uri: helloWorldLibUri, + let expectedTargets: [ExpectedTargetInfo] = [ + ExpectedTargetInfo(displayName: "//HelloWorld:ExpandedTemplate", language: .swift, dependencyLabels: []), + ExpectedTargetInfo(displayName: "//HelloWorld:GeneratedDummy", language: .swift, dependencyLabels: []), + ExpectedTargetInfo( displayName: "//HelloWorld:HelloWorldLib", - dependencies: [todoModelsUri, todoObjCSupportUri, expandedTemplateUri, generatedDummyUri] + language: .swift, + dependencyLabels: [ + "//HelloWorld:TodoModels", "//HelloWorld:TodoObjCSupport", + "//HelloWorld:ExpandedTemplate", "//HelloWorld:GeneratedDummy", + ] ), - makeExpectedTarget( - uri: helloWorldTestsLibUri, + ExpectedTargetInfo( displayName: "//HelloWorld:HelloWorldTestsLib", - dependencies: [helloWorldLibUri] + language: .swift, + dependencyLabels: ["//HelloWorld:HelloWorldLib"] ), - makeExpectedTarget( - uri: macAppLibUri, + ExpectedTargetInfo( displayName: "//HelloWorld:MacAppLib", - dependencies: [todoModelsUri] + language: .swift, + dependencyLabels: ["//HelloWorld:TodoModels"] ), - makeExpectedTarget( - uri: macAppTestsLibUri, + ExpectedTargetInfo( displayName: "//HelloWorld:MacAppTestsLib", - dependencies: [macAppLibUri] + language: .swift, + dependencyLabels: ["//HelloWorld:MacAppLib"] ), - makeExpectedTarget( - uri: macCLIAppLibUri, + ExpectedTargetInfo( displayName: "//HelloWorld:MacCLIAppLib", - dependencies: [todoModelsUri] + language: .swift, + dependencyLabels: ["//HelloWorld:TodoModels"] ), - makeExpectedTarget(uri: todoModelsUri, displayName: "//HelloWorld:TodoModels"), - makeExpectedTarget( - uri: todoObjCSupportUri, + ExpectedTargetInfo(displayName: "//HelloWorld:TodoModels", language: .swift, dependencyLabels: []), + ExpectedTargetInfo( displayName: "//HelloWorld:TodoObjCSupport", language: .objective_c, - dependencies: [todoCSupport] + dependencyLabels: ["//HelloWorld:TodoCSupport"] ), - makeExpectedTarget( - uri: todoCSupport, - displayName: "//HelloWorld:TodoCSupport", - language: .cpp - ), - makeExpectedTarget( - uri: watchAppLibUri, + ExpectedTargetInfo(displayName: "//HelloWorld:TodoCSupport", language: .cpp, dependencyLabels: []), + ExpectedTargetInfo( displayName: "//HelloWorld:WatchAppLib", - dependencies: [todoModelsUri] + language: .swift, + dependencyLabels: ["//HelloWorld:TodoModels"] ), - makeExpectedTarget( - uri: watchAppTestsLibUri, + ExpectedTargetInfo( displayName: "//HelloWorld:WatchAppTestsLib", - dependencies: [watchAppLibUri] + language: .swift, + dependencyLabels: ["//HelloWorld:WatchAppLib"] ), ] - #expect(result.buildTargets.count == expectedBuildTargets.count) - // Build a lookup map for actual targets by displayName - let actualTargetsByName = Dictionary( - uniqueKeysWithValues: result.buildTargets.compactMap { target -> (String, BuildTarget)? in - guard let name = target.displayName else { return nil } - return (name, target) - } - ) + // Group actual targets by displayName (same target can appear with multiple configs) + var actualTargetsByName: [String: [BuildTarget]] = [:] + for target in result.buildTargets { + guard let name = target.displayName else { continue } + actualTargetsByName[name, default: []].append(target) + } - // Verify each expected target matches the actual target - for expected in expectedBuildTargets { - let displayName = try #require(expected.displayName) - let actual = try #require(actualTargetsByName[displayName], "Missing target: \(displayName)") + // Verify all expected targets exist and have correct properties + for expected in expectedTargets { + let targets = try #require( + actualTargetsByName[expected.displayName], + "Missing target: \(expected.displayName)" + ) + #expect(!targets.isEmpty, "No targets found for \(expected.displayName)") - #expect(actual.id == expected.id, "ID mismatch for \(displayName)") - #expect(actual.displayName == expected.displayName, "displayName mismatch for \(displayName)") - #expect(actual.languageIds == expected.languageIds, "languageIds mismatch for \(displayName)") + // Verify each variant has the correct language + for target in targets { + #expect( + target.languageIds == [expected.language], + "languageIds mismatch for \(expected.displayName)" + ) + } - let actualDeps = Set(actual.dependencies.map { $0.uri }) - let expectedDeps = Set(expected.dependencies.map { $0.uri }) - #expect(actualDeps == expectedDeps, "dependencies mismatch for \(displayName)") + // Verify dependencies by looking up the labels from the dependency URIs + // Each variant should have dependencies pointing to targets with the expected labels + for target in targets { + let actualDepLabels = Set( + target.dependencies.compactMap { depId -> String? in + result.bspURIsToBazelLabelsMap[depId.uri] + } + ) + #expect( + actualDepLabels == expected.dependencyLabels, + "dependencies mismatch for \(expected.displayName): got \(actualDepLabels), expected \(expected.dependencyLabels)" + ) + } } - // Top level targets + // Verify we have the expected unique target labels + let expectedLabels = Set(expectedTargets.map { $0.displayName }) + let actualLabels = Set(actualTargetsByName.keys) + #expect(actualLabels == expectedLabels, "Target label sets don't match") + + // Top level targets - verify label and rule type (config IDs are assigned during parsing) let expectedTopLevelTargets: [(String, TopLevelRuleType)] = [ ("//HelloWorld:HelloWorldMacTests", .macosUnitTest), ("//HelloWorld:HelloWorldTests", .iosUnitTest), @@ -182,86 +165,70 @@ struct BazelTargetQuerierParserImplTests { ] #expect(result.topLevelTargets.count == expectedTopLevelTargets.count) for (index, expected) in expectedTopLevelTargets.enumerated() { - #expect(result.topLevelTargets[index] == expected) + let actual = result.topLevelTargets[index] + #expect(actual.0 == expected.0) + #expect(actual.1 == expected.1) } - // Top level label to rule map - let expectedTopLevelLabelToRuleMap: [String: TopLevelRuleType] = [ - "//HelloWorld:HelloWorld": .iosApplication, - "//HelloWorld:HelloWorldMacApp": .macosApplication, - "//HelloWorld:HelloWorldMacCLIApp": .macosCommandLineApplication, - "//HelloWorld:HelloWorldMacTests": .macosUnitTest, - "//HelloWorld:HelloWorldTests": .iosUnitTest, - "//HelloWorld:HelloWorldWatchApp": .watchosApplication, - "//HelloWorld:HelloWorldWatchExtension": .watchosExtension, - "//HelloWorld:HelloWorldWatchTests": .watchosUnitTest, - ] - #expect(result.topLevelLabelToRuleMap == expectedTopLevelLabelToRuleMap) - - // BSP URIs to Bazel Labels map - #expect( - result.bspURIsToBazelLabelsMap == [ - expandedTemplateUri: "//HelloWorld:ExpandedTemplate", - generatedDummyUri: "//HelloWorld:GeneratedDummy", - helloWorldLibUri: "//HelloWorld:HelloWorldLib", - helloWorldTestsLibUri: "//HelloWorld:HelloWorldTestsLib", - macAppLibUri: "//HelloWorld:MacAppLib", - macAppTestsLibUri: "//HelloWorld:MacAppTestsLib", - macCLIAppLibUri: "//HelloWorld:MacCLIAppLib", - todoModelsUri: "//HelloWorld:TodoModels", - todoObjCSupportUri: "//HelloWorld:TodoObjCSupport", - todoCSupport: "//HelloWorld:TodoCSupport", - watchAppLibUri: "//HelloWorld:WatchAppLib", - watchAppTestsLibUri: "//HelloWorld:WatchAppTestsLib", - ] - ) + // Verify bspURIsToBazelLabelsMap contains all expected labels + // With multi-variant support, the same label may have multiple URIs + let actualLabelSet = Set(result.bspURIsToBazelLabelsMap.values) + #expect(actualLabelSet == expectedLabels, "bspURIsToBazelLabelsMap labels don't match") - #expect(result.bspURIsToSrcsMap.keys.count == 12) - #expect(result.srcToBspURIsMap.count == 23) + // Verify counts - with multi-variant support, targets can have multiple URIs (one per config) + #expect(result.bspURIsToSrcsMap.keys.count == 14, "bspURIsToSrcsMap should have 14 target URIs") + #expect(result.srcToBspURIsMap.count == 23, "srcToBspURIsMap should have 23 source files") - // Bazel label to parent config map - verify labels map to configs - #expect(result.bazelLabelToParentConfigMap.count == 20) + // BSP URI to parent config map - verify URIs map to configs + #expect(result.bspUriToParentConfigMap.count == 14, "bspUriToParentConfigMap should have 14 entries") // Helper to get parent labels for a given label through the config mapping - func getParentLabels(for label: String) -> Set { - guard let configHash = result.bazelLabelToParentConfigMap[label], - let parentLabels = result.configurationToTopLevelLabelsMap[configHash] - else { - return [] + // Finds all URIs for a label and returns the union of their parent labels + func getParentLabels(forLabel label: String) -> Set { + var parentLabels = Set() + for (uri, targetLabel) in result.bspURIsToBazelLabelsMap where targetLabel == label { + guard let configHash = result.bspUriToParentConfigMap[uri], + let labels = result.configurationToTopLevelLabelsMap[configHash] + else { + continue + } + parentLabels.formUnion(labels) } - return Set(parentLabels) + return parentLabels } + // iOS targets should have iOS top-level parents #expect( - getParentLabels(for: "//HelloWorld:ExpandedTemplate") + getParentLabels(forLabel: "//HelloWorld:ExpandedTemplate") == Set([ "//HelloWorld:HelloWorldTests", "//HelloWorld:HelloWorld", ]) ) #expect( - getParentLabels(for: "//HelloWorld:GeneratedDummy") + getParentLabels(forLabel: "//HelloWorld:GeneratedDummy") == Set([ "//HelloWorld:HelloWorldTests", "//HelloWorld:HelloWorld", ]) ) #expect( - getParentLabels(for: "//HelloWorld:HelloWorldLib") + getParentLabels(forLabel: "//HelloWorld:HelloWorldLib") == Set([ "//HelloWorld:HelloWorldTests", "//HelloWorld:HelloWorld", ]) ) #expect( - getParentLabels(for: "//HelloWorld:HelloWorldTestsLib") + getParentLabels(forLabel: "//HelloWorld:HelloWorldTestsLib") == Set([ "//HelloWorld:HelloWorldTests", "//HelloWorld:HelloWorld", ]) ) + // macOS targets #expect( - getParentLabels(for: "//HelloWorld:MacAppLib") + getParentLabels(forLabel: "//HelloWorld:MacAppLib") == Set([ "//HelloWorld:HelloWorldMacTests", "//HelloWorld:HelloWorldMacCLIApp", @@ -269,7 +236,7 @@ struct BazelTargetQuerierParserImplTests { ]) ) #expect( - getParentLabels(for: "//HelloWorld:MacAppTestsLib") + getParentLabels(forLabel: "//HelloWorld:MacAppTestsLib") == Set([ "//HelloWorld:HelloWorldMacTests", "//HelloWorld:HelloWorldMacCLIApp", @@ -277,30 +244,37 @@ struct BazelTargetQuerierParserImplTests { ]) ) #expect( - getParentLabels(for: "//HelloWorld:MacCLIAppLib") + getParentLabels(forLabel: "//HelloWorld:MacCLIAppLib") == Set([ "//HelloWorld:HelloWorldMacTests", "//HelloWorld:HelloWorldMacCLIApp", "//HelloWorld:HelloWorldMacApp", ]) ) + // TodoModels is used by multiple platforms (iOS, macOS, watchOS) #expect( - getParentLabels(for: "//HelloWorld:TodoModels") + getParentLabels(forLabel: "//HelloWorld:TodoModels") == Set([ "//HelloWorld:HelloWorldMacTests", "//HelloWorld:HelloWorldMacCLIApp", "//HelloWorld:HelloWorldMacApp", + "//HelloWorld:HelloWorldWatchApp", + "//HelloWorld:HelloWorldWatchTests", + "//HelloWorld:HelloWorldWatchExtension", + "//HelloWorld:HelloWorldTests", + "//HelloWorld:HelloWorld", ]) ) #expect( - getParentLabels(for: "//HelloWorld:TodoObjCSupport") + getParentLabels(forLabel: "//HelloWorld:TodoObjCSupport") == Set([ "//HelloWorld:HelloWorldTests", "//HelloWorld:HelloWorld", ]) ) + // watchOS targets #expect( - getParentLabels(for: "//HelloWorld:WatchAppLib") + getParentLabels(forLabel: "//HelloWorld:WatchAppLib") == Set([ "//HelloWorld:HelloWorldWatchExtension", "//HelloWorld:HelloWorldWatchApp", @@ -308,7 +282,7 @@ struct BazelTargetQuerierParserImplTests { ]) ) #expect( - getParentLabels(for: "//HelloWorld:WatchAppTestsLib") + getParentLabels(forLabel: "//HelloWorld:WatchAppTestsLib") == Set([ "//HelloWorld:HelloWorldWatchExtension", "//HelloWorld:HelloWorldWatchApp", @@ -322,15 +296,16 @@ struct BazelTargetQuerierParserImplTests { let parser = BazelTargetQuerierParserImpl() // These details are meant to match the provided aquery pb example. - let topLevelTargets: [(String, TopLevelRuleType)] = [ - ("//HelloWorld:HelloWorld", .iosApplication), - ("//HelloWorld:HelloWorldMacApp", .macosApplication), - ("//HelloWorld:HelloWorldMacCLIApp", .macosCommandLineApplication), - ("//HelloWorld:HelloWorldMacTests", .macosUnitTest), - ("//HelloWorld:HelloWorldTests", .iosUnitTest), - ("//HelloWorld:HelloWorldWatchApp", .watchosApplication), - ("//HelloWorld:HelloWorldWatchExtension", .watchosExtension), - ("//HelloWorld:HelloWorldWatchTests", .watchosUnitTest), + // Config IDs: 1 = iOS, 2 = macOS, 3 = watchOS + let topLevelTargets: [(String, TopLevelRuleType, UInt32)] = [ + ("//HelloWorld:HelloWorld", .iosApplication, 1), + ("//HelloWorld:HelloWorldMacApp", .macosApplication, 2), + ("//HelloWorld:HelloWorldMacCLIApp", .macosCommandLineApplication, 2), + ("//HelloWorld:HelloWorldMacTests", .macosUnitTest, 2), + ("//HelloWorld:HelloWorldTests", .iosUnitTest, 1), + ("//HelloWorld:HelloWorldWatchApp", .watchosApplication, 3), + ("//HelloWorld:HelloWorldWatchExtension", .watchosExtension, 3), + ("//HelloWorld:HelloWorldWatchTests", .watchosUnitTest, 3), ] let result = try parser.processAquery( @@ -338,93 +313,45 @@ struct BazelTargetQuerierParserImplTests { topLevelTargets: topLevelTargets ) - #expect(result.topLevelLabelToConfigMap.count == 8) + // 3 unique config IDs (iOS, macOS, watchOS) + #expect(result.topLevelConfigIdToInfoMap.count == 3) + // iOS config (config ID 1) #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorld"] + result.topLevelConfigIdToInfoMap[1] == BazelTargetConfigurationInfo( configurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0-applebin_ios-ST-faa571ec622f", effectiveConfigurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0", minimumOsVersion: "17.0", platform: "ios", - cpuArch: "sim_arm64" - ) - ) - - #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldMacApp"] - == BazelTargetConfigurationInfo( - configurationName: "darwin_arm64-dbg-macos-arm64-min15.0-applebin_macos-ST-d1334902beb6", - effectiveConfigurationName: "darwin_arm64-dbg-macos-arm64-min15.0", - minimumOsVersion: "15.0", - platform: "darwin", - cpuArch: "arm64" - ) - ) - - #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldMacCLIApp"] - == BazelTargetConfigurationInfo( - configurationName: "darwin_arm64-dbg-macos-arm64-min15.0-applebin_macos-ST-d1334902beb6", - effectiveConfigurationName: "darwin_arm64-dbg-macos-arm64-min15.0", - minimumOsVersion: "15.0", - platform: "darwin", - cpuArch: "arm64" + cpuArch: "sim_arm64", + sdkName: "iphonesimulator" ) ) + // macOS config (config ID 2) #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldMacTests"] + result.topLevelConfigIdToInfoMap[2] == BazelTargetConfigurationInfo( configurationName: "darwin_arm64-dbg-macos-arm64-min15.0-applebin_macos-ST-d1334902beb6", effectiveConfigurationName: "darwin_arm64-dbg-macos-arm64-min15.0", minimumOsVersion: "15.0", platform: "darwin", - cpuArch: "arm64" - ) - ) - - #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldTests"] - == BazelTargetConfigurationInfo( - configurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0-applebin_ios-ST-faa571ec622f", - effectiveConfigurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0", - minimumOsVersion: "17.0", - platform: "ios", - cpuArch: "sim_arm64" - ) - ) - - #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldWatchApp"] - == BazelTargetConfigurationInfo( - configurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0-applebin_watchos-ST-74f4ed91ef5d", - effectiveConfigurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0", - minimumOsVersion: "7.0", - platform: "watchos", - cpuArch: "x86_64" - ) - ) - - #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldWatchExtension"] - == BazelTargetConfigurationInfo( - configurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0-applebin_watchos-ST-74f4ed91ef5d", - effectiveConfigurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0", - minimumOsVersion: "7.0", - platform: "watchos", - cpuArch: "x86_64" + cpuArch: "arm64", + sdkName: "macosx" ) ) + // watchOS config (config ID 3) #expect( - result.topLevelLabelToConfigMap["//HelloWorld:HelloWorldWatchTests"] + result.topLevelConfigIdToInfoMap[3] == BazelTargetConfigurationInfo( configurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0-applebin_watchos-ST-74f4ed91ef5d", effectiveConfigurationName: "watchos_x86_64-dbg-watchos-x86_64-min7.0", minimumOsVersion: "7.0", platform: "watchos", - cpuArch: "x86_64" + cpuArch: "x86_64", + sdkName: "watchsimulator" ) ) } diff --git a/Tests/SourceKitBazelBSPTests/BazelTargetQuerierTests.swift b/Tests/SourceKitBazelBSPTests/BazelTargetQuerierTests.swift index 527e19b0..fe17bc5d 100644 --- a/Tests/SourceKitBazelBSPTests/BazelTargetQuerierTests.swift +++ b/Tests/SourceKitBazelBSPTests/BazelTargetQuerierTests.swift @@ -33,16 +33,15 @@ struct BazelTargetQuerierTests { bspURIsToBazelLabelsMap: [:], bspURIsToSrcsMap: [:], srcToBspURIsMap: [:], - topLevelLabelToRuleMap: [:], configurationToTopLevelLabelsMap: [:], - bazelLabelToParentConfigMap: [:] + bspUriToParentConfigMap: [:] ) private static let emptyProcessedAqueryResult = ProcessedAqueryResult( targets: [:], actions: [:], configurations: [:], - topLevelLabelToConfigMap: [:] + topLevelConfigIdToInfoMap: [:] ) private static func makeInitializedConfig( @@ -288,7 +287,7 @@ struct BazelTargetQuerierTests { runnerMock.setResponse(for: expectedCommand, cwd: Self.mockRootUri, response: exampleAqueryOutput) _ = try querier.aquery( - topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication)], + topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication, 1)], config: config, mnemonics: ["SwiftCompile"] ) @@ -312,8 +311,8 @@ struct BazelTargetQuerierTests { _ = try querier.aquery( topLevelTargets: [ - ("//HelloWorld:HelloWorld", .iosApplication), - ("//Tests:Tests", .iosUnitTest), + ("//HelloWorld:HelloWorld", .iosApplication, 1), + ("//Tests:Tests", .iosUnitTest, 2), ], config: config, mnemonics: ["SwiftCompile", "ObjcCompile"] @@ -347,7 +346,7 @@ struct BazelTargetQuerierTests { func run(mnemonics: [String]) throws { _ = try querier.aquery( - topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication)], + topLevelTargets: [("//HelloWorld:HelloWorld", .iosApplication, 1)], config: config, mnemonics: mnemonics ) diff --git a/Tests/SourceKitBazelBSPTests/Fakes/BazelTargetQuerierParserFake.swift b/Tests/SourceKitBazelBSPTests/Fakes/BazelTargetQuerierParserFake.swift index 868d53ce..b2fafd5a 100644 --- a/Tests/SourceKitBazelBSPTests/Fakes/BazelTargetQuerierParserFake.swift +++ b/Tests/SourceKitBazelBSPTests/Fakes/BazelTargetQuerierParserFake.swift @@ -43,7 +43,7 @@ final class BazelTargetQuerierParserFake: BazelTargetQuerierParser { func processAquery( from data: Data, - topLevelTargets: [(String, TopLevelRuleType)], + topLevelTargets: [(String, TopLevelRuleType, UInt32)], ) throws -> ProcessedAqueryResult { guard let mockAqueryResult else { unimplemented()