From b9177fd749a2117a6acd2eebed50c62e30c78f11 Mon Sep 17 00:00:00 2001 From: Matthias Schwab Date: Tue, 14 Apr 2026 17:13:09 +0200 Subject: [PATCH 1/3] Fix config mismatch for targets under multiple top-level parents When a dependency exists under multiple top-level targets (e.g., an app and its extensions), each top-level target gets a unique settings transition hash (-ST-) in its config mnemonic. The parent config stored during cquery may reference a different ST hash than the one present in the aquery actions, causing "No relevant target actions found" errors for otherwise valid targets. Fall back to matching without the ST hash when exact mnemonic matching fails. This preserves the platform/arch/minOS constraint while tolerating ST hash differences across top-level targets. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../BazelTargetCompilerArgsExtractor.swift | 34 ++++++++++++++++--- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift index d3f5054..eba1bee 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift @@ -228,6 +228,17 @@ final class BazelTargetCompilerArgsExtractor { } return config.mnemonic == platformInfo.topLevelParentConfig.configurationName } + // When a dependency exists under multiple top-level targets, each gets a unique + // settings transition hash (the -ST- suffix in the config mnemonic). + // The parent config stored during cquery may not match the one from aquery. + // Fall back to matching without the ST hash — same platform/arch/minOS. + if candidateActions.isEmpty { + let requestedBase = platformInfo.topLevelParentConfig.configurationName.strippingSettingsTransitionHash + candidateActions = actions.filter { + guard let config = aquery.configurations[$0.configurationID] else { return false } + return config.mnemonic.strippingSettingsTransitionHash == requestedBase + } + } let contentBeingQueried: String switch strategy { case .swiftModule(_), .cHeader: @@ -245,13 +256,12 @@ final class BazelTargetCompilerArgsExtractor { return false } } - guard candidateActions.count > 0 else { + // After all filtering, multiple candidates means different ST variants + // for the same platform/file — compiler args are effectively identical. + guard let action = candidateActions.first else { throw BazelTargetCompilerArgsExtractorError.relevantTargetActionsNotFound(contentBeingQueried) } - guard candidateActions.count == 1 else { - throw BazelTargetCompilerArgsExtractorError.multipleTargetActions(contentBeingQueried, target.id) - } - return candidateActions[0] + return action } func clearCache() { @@ -446,3 +456,17 @@ extension BazelTargetCompilerArgsExtractor { lines[idx + 1] = new } } + +private extension String { + /// Strips the Bazel settings transition hash suffix from a configuration mnemonic. + /// + /// Bazel appends a `-ST-` suffix to configuration mnemonics to distinguish builds + /// that share the same platform/arch/minOS but have different build settings due to + /// different top-level targets (e.g., an app vs. an extension). The hash is deterministic + /// for a given set of settings but differs across top-level targets. + /// + /// See: https://bazel.build/extending/config#user-defined-transitions + var strippingSettingsTransitionHash: String { + self.replacingOccurrences(of: #"-ST-[a-f0-9]+$"#, with: "", options: .regularExpression) + } +} From 9ad6827b5cdcf4cf99ebdd8421347a49d0fc366e Mon Sep 17 00:00:00 2001 From: Matthias Schwab Date: Tue, 14 Apr 2026 17:39:17 +0200 Subject: [PATCH 2/3] Fix lint: use fileprivate on member instead of private on extension --- .../SKOptions/BazelTargetCompilerArgsExtractor.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift index eba1bee..9557619 100644 --- a/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift +++ b/Sources/SourceKitBazelBSP/RequestHandlers/SKOptions/BazelTargetCompilerArgsExtractor.swift @@ -457,7 +457,7 @@ extension BazelTargetCompilerArgsExtractor { } } -private extension String { +extension String { /// Strips the Bazel settings transition hash suffix from a configuration mnemonic. /// /// Bazel appends a `-ST-` suffix to configuration mnemonics to distinguish builds @@ -466,7 +466,7 @@ private extension String { /// for a given set of settings but differs across top-level targets. /// /// See: https://bazel.build/extending/config#user-defined-transitions - var strippingSettingsTransitionHash: String { + fileprivate var strippingSettingsTransitionHash: String { self.replacingOccurrences(of: #"-ST-[a-f0-9]+$"#, with: "", options: .regularExpression) } } From 3ecb4a9132b587b676533387cdaeda201f8e0063 Mon Sep 17 00:00:00 2001 From: Matthias Schwab Date: Tue, 14 Apr 2026 17:42:22 +0200 Subject: [PATCH 3/3] Add tests for ST-hash fallback in config matching Tests that extractCompilerArgs succeeds when the platform info has a different ST hash than the aquery actions, for both Swift and ObjC targets. --- ...azelTargetCompilerArgsExtractorTests.swift | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift b/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift index 51d14c5..1a76823 100644 --- a/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift +++ b/Tests/SourceKitBazelBSPTests/BazelTargetCompilerArgsExtractorTests.swift @@ -228,6 +228,59 @@ struct BazelTargetCompilerArgsExtractorTests { #expect(language == "objective-c") } + @Test + func swiftModuleWithDifferentSTHash() throws { + let extractor = Self.makeMockExtractor() + let testSourceUri = URI( + filePath: "/Users/user/Documents/demo-ios-project/HelloWorld/HelloWorldLib/Sources/AddTodoView.swift", + isDirectory: false + ) + // Use a different ST hash than the one in the aquery (2842469f5300). + // This simulates a dependency that exists under multiple top-level targets + // where the cquery stored a different parent's config than the aquery has. + let differentSTConfig = BazelTargetConfigurationInfo( + configurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0-ST-aaaaaaaaaaaa", + minimumOsVersion: "17.0", + platform: "iphonesimulator", + cpuArch: "sim_arm64", + sdkName: "iphonesimulator" + ) + let result = try extractor.extractCompilerArgs( + fromAquery: aqueryResult, + forTarget: BazelTargetPlatformInfo( + label: "//HelloWorld:HelloWorldLib", + topLevelParentLabel: "//HelloWorld:HelloWorld", + topLevelParentConfig: differentSTConfig + ), + withStrategy: .swiftModule(testSourceUri), + indexOutputPath: nil + ) + #expect(!result.isEmpty) + } + + @Test + func objcFileWithDifferentSTHash() throws { + let extractor = Self.makeMockExtractor() + let differentSTConfig = BazelTargetConfigurationInfo( + configurationName: "ios_sim_arm64-dbg-ios-sim_arm64-min17.0-ST-bbbbbbbbbbbb", + minimumOsVersion: "17.0", + platform: "iphonesimulator", + cpuArch: "sim_arm64", + sdkName: "iphonesimulator" + ) + let result = try extractor.extractCompilerArgs( + fromAquery: aqueryResult, + forTarget: BazelTargetPlatformInfo( + label: "//HelloWorld:TodoObjCSupport", + topLevelParentLabel: "//HelloWorld:HelloWorld", + topLevelParentConfig: differentSTConfig + ), + withStrategy: .cImpl("HelloWorld/TodoObjCSupport/Sources/SKObjCUtils.m", "objective-c"), + indexOutputPath: nil + ) + #expect(!result.isEmpty) + } + @Test func missingSwiftModule() throws { let extractor = Self.makeMockExtractor()