Skip to content

Commit 9df6203

Browse files
authored
Merge pull request #2230 from Bigcheese/dev/negdeps-invalidated-paths
2 parents 6a0da30 + d5bdee1 commit 9df6203

7 files changed

Lines changed: 176 additions & 1 deletion

File tree

‎Sources/CSwiftScan/include/swiftscan_header.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ typedef struct {
217217
(*swiftscan_clang_detail_get_context_hash)(swiftscan_module_details_t);
218218
swiftscan_string_set_t *
219219
(*swiftscan_clang_detail_get_command_line)(swiftscan_module_details_t);
220+
swiftscan_string_set_t *
221+
(*swiftscan_clang_detail_get_directory_deps)(swiftscan_module_details_t);
220222
swiftscan_string_ref_t
221223
(*swiftscan_clang_detail_get_module_cache_key)(swiftscan_module_details_t);
222224

@@ -264,6 +266,8 @@ typedef struct {
264266
//=== Scanner Functions ---------------------------------------------------===//
265267
swiftscan_scanner_t (*swiftscan_scanner_create)(void);
266268
void (*swiftscan_scanner_dispose)(swiftscan_scanner_t);
269+
void (*swiftscan_scanner_add_invalidated_paths)(swiftscan_scanner_t,
270+
const char **, size_t);
267271
swiftscan_dependency_graph_t
268272
(*swiftscan_dependency_graph_create)(swiftscan_scanner_t, swiftscan_scan_invocation_t);
269273
swiftscan_import_set_t

‎Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyGraph.swift‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,11 @@ public struct ClangModuleDetails: Codable, Hashable {
208208

209209
/// The module cache key of the output module.
210210
public var moduleCacheKey: String?
211+
212+
/// Directories whose contents this module enumerated, such as umbrella
213+
/// directories. Report changes to them with
214+
/// `InterModuleDependencyOracle.addInvalidatedPaths`.
215+
public var directoryDependencies: [TextualVirtualPath]?
211216
}
212217

213218
public struct ModuleInfo: Codable, Hashable {

‎Sources/SwiftDriver/ExplicitModuleBuilds/InterModuleDependencies/InterModuleDependencyOracle.swift‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,25 @@ public class InterModuleDependencyOracle {
147147
return swiftScan.supportsSeparateImportOnlyDependencies
148148
}
149149

150+
@_spi(Testing) public func supportsDirectoryDependencies() throws -> Bool {
151+
guard let swiftScan = swiftScanLibInstance else {
152+
fatalError("Attempting to query supported scanner API with no scanner instance.")
153+
}
154+
return swiftScan.supportsDirectoryDependencies
155+
}
156+
157+
/// Report that `paths` changed since the previous build. Must be called
158+
/// before the first scan, with paths spelled as in
159+
/// `ClangModuleDetails.directoryDependencies`.
160+
public func addInvalidatedPaths(_ paths: [AbsolutePath]) throws {
161+
guard let swiftScan = swiftScanLibInstance else {
162+
throw DependencyScanningError.failedToInstantiateScanner
163+
}
164+
guard !paths.isEmpty else { return }
165+
// No need to synchronize with `queue`, the scanner is thread-safe here.
166+
swiftScan.addInvalidatedPaths(paths.map { $0.pathString })
167+
}
168+
150169
public func getOrCreateCAS(pluginPath: AbsolutePath?, onDiskPath: AbsolutePath?, pluginOptions: [(String, String)]) throws -> SwiftScanCAS {
151170
guard let swiftScan = swiftScanLibInstance else {
152171
fatalError("Attempting to reset scanner cache with no scanner instance.")

‎Sources/SwiftDriver/SwiftScan/DependencyGraphBuilder.swift‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,17 @@ private extension SwiftScan {
347347
let moduleCacheKey = supportsCaching ? try getOptionalStringDetail(from: moduleDetailsRef,
348348
using: api.swiftscan_clang_detail_get_module_cache_key) : nil
349349

350+
let directoryDependencies =
351+
supportsDirectoryDependencies
352+
? try getOptionalPathArrayDetail(from: moduleDetailsRef,
353+
using: api.swiftscan_clang_detail_get_directory_deps!)
354+
: nil
355+
350356
return ClangModuleDetails(moduleMapPath: moduleMapPath,
351357
contextHash: contextHash,
352358
commandLine: commandLine,
353-
moduleCacheKey: moduleCacheKey)
359+
moduleCacheKey: moduleCacheKey,
360+
directoryDependencies: directoryDependencies)
354361
}
355362
}
356363

‎Sources/SwiftDriver/SwiftScan/SwiftScan.swift‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,11 @@ private extension String {
325325
return api.swiftscan_module_info_get_library_level != nil
326326
}
327327

328+
@_spi(Testing) public var supportsDirectoryDependencies : Bool {
329+
return api.swiftscan_clang_detail_get_directory_deps != nil &&
330+
api.swiftscan_scanner_add_invalidated_paths != nil
331+
}
332+
328333
internal func mapToDriverDiagnosticPayload(_ diagnosticSetRef: UnsafeMutablePointer<swiftscan_diagnostic_set_t>) throws -> [ScannerDiagnosticPayload] {
329334
var result: [ScannerDiagnosticPayload] = []
330335
let diagnosticRefArray = Array(UnsafeBufferPointer(start: diagnosticSetRef.pointee.diagnostics,
@@ -360,6 +365,16 @@ private extension String {
360365
api.swiftscan_string_set_dispose != nil
361366
}
362367

368+
internal func addInvalidatedPaths(_ paths: [String]) {
369+
guard let addInvalidated = api.swiftscan_scanner_add_invalidated_paths,
370+
!paths.isEmpty else {
371+
return
372+
}
373+
withArrayOfCStrings(paths) { pathArray in
374+
addInvalidated(scanner, pathArray, paths.count)
375+
}
376+
}
377+
363378
@_spi(Testing) public func querySupportedArguments() throws -> Set<String> {
364379
precondition(canQuerySupportedArguments())
365380
if let queryResultStrings = api.swiftscan_compiler_supported_arguments_query!() {
@@ -508,6 +523,12 @@ private extension swiftscan_functions_t {
508523
self.swiftscan_clang_detail_get_module_cache_key =
509524
loadOptional("swiftscan_clang_detail_get_module_cache_key")
510525

526+
// Directory dependencies
527+
self.swiftscan_clang_detail_get_directory_deps =
528+
loadOptional("swiftscan_clang_detail_get_directory_deps")
529+
self.swiftscan_scanner_add_invalidated_paths =
530+
loadOptional("swiftscan_scanner_add_invalidated_paths")
531+
511532
self.swiftscan_cas_options_create = loadOptional("swiftscan_cas_options_create")
512533
self.swiftscan_cas_options_set_plugin_path = loadOptional("swiftscan_cas_options_set_plugin_path")
513534
self.swiftscan_cas_options_set_ondisk_path = loadOptional("swiftscan_cas_options_set_ondisk_path")
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2026 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import Foundation
16+
@_spi(Testing) import SwiftDriver
17+
import TSCBasic
18+
import TestUtilities
19+
import Testing
20+
21+
@Suite struct DirectoryDependenciesTests {
22+
/// Write a Clang module with an umbrella directory, which gives it a directory
23+
/// dependency.
24+
private func writeUmbrellaDirModule(at root: AbsolutePath) throws {
25+
let include = root.appending(component: "include")
26+
try localFileSystem.createDirectory(include.appending(component: "sub"), recursive: true)
27+
try localFileSystem.writeFileContents(include.appending(component: "module.modulemap")) {
28+
$0.send("module UmbDir { umbrella \"sub\" module * { export * } }\n")
29+
}
30+
try localFileSystem.writeFileContents(
31+
include.appending(components: "sub", "a.h")) { $0.send("void a(void);\n") }
32+
try localFileSystem.writeFileContents(
33+
root.appending(component: "test.swift")) { $0.send("import UmbDir\n") }
34+
}
35+
36+
/// Scan UmbDir as a separate build, with a fresh oracle and `paths` reported
37+
/// first.
38+
private func scanBuild(root: AbsolutePath,
39+
invalidating paths: [AbsolutePath] = []) throws
40+
-> ModuleInfo {
41+
let (stdlibPath, shimsPath, toolchain, _) = try getDriverArtifactsForScanning()
42+
let oracle = InterModuleDependencyOracle()
43+
let scanLibPath = try #require(try toolchain.lookupSwiftScanLib())
44+
try oracle.verifyOrCreateScannerInstance(swiftScanLibPath: scanLibPath)
45+
try oracle.addInvalidatedPaths(paths)
46+
var diagnostics: [ScannerDiagnosticPayload] = []
47+
let graph = try oracle.getDependencies(
48+
workingDirectory: root,
49+
commandLine: [
50+
"-scan-dependencies",
51+
"-module-name", "DirDepTest",
52+
"-module-cache-path", root.appending(component: "mcp").pathString,
53+
"-I", stdlibPath.nativePathString(escaped: false),
54+
"-I", shimsPath.nativePathString(escaped: false),
55+
"-I", root.appending(component: "include").pathString,
56+
"-disable-implicit-concurrency-module-import",
57+
"-disable-implicit-string-processing-module-import",
58+
root.appending(component: "test.swift").pathString,
59+
] + ((try? Driver.sdkArgumentsForTesting()) ?? []),
60+
diagnostics: &diagnostics)
61+
return try #require(graph.modules[.clang("UmbDir")])
62+
}
63+
64+
/// The basenames of the headers `info` was built from.
65+
private func headers(_ info: ModuleInfo) -> Set<String> {
66+
Set((info.sourceFiles ?? []).filter { $0.hasSuffix(".h") }.map {
67+
(try? AbsolutePath(validating: $0))?.basename ?? $0
68+
})
69+
}
70+
71+
@Test(.requireScannerSupportsDirectoryDependencies())
72+
func reportsDirectoryDependencies() throws {
73+
try withTemporaryDirectory { root in
74+
try writeUmbrellaDirModule(at: root)
75+
let info = try scanBuild(root: root)
76+
guard case .clang(let details) = info.details else {
77+
Issue.record("UmbDir is not a Clang module")
78+
return
79+
}
80+
let reported = (details.directoryDependencies ?? []).map {
81+
VirtualPath.lookup($0.path).description
82+
}
83+
#expect(reported == [root.appending(components: "include", "sub").pathString])
84+
}
85+
}
86+
87+
@Test(.requireScannerSupportsDirectoryDependencies())
88+
func invalidatedDirectoryRebuildsModule() throws {
89+
try withTemporaryDirectory { root in
90+
try writeUmbrellaDirModule(at: root)
91+
let globbedDir = root.appending(components: "include", "sub")
92+
93+
let first = try headers(scanBuild(root: root))
94+
#expect(first.contains("a.h"))
95+
#expect(!first.contains("b.h"))
96+
97+
// Add a header. Without a report the stale module is reused. Modules
98+
// built in the same second a scan starts count as up to date, so wait.
99+
Thread.sleep(forTimeInterval: 1)
100+
try localFileSystem.writeFileContents(
101+
globbedDir.appending(component: "b.h")) { $0.send("void b(void);\n") }
102+
let stale = try headers(scanBuild(root: root))
103+
#expect(!stale.contains("b.h"),
104+
"expected the stale cached module to be reused without invalidation")
105+
106+
// Reporting the directory picks up the header.
107+
let rebuilt = try headers(scanBuild(root: root, invalidating: [globbedDir]))
108+
#expect(rebuilt.contains("a.h"))
109+
#expect(rebuilt.contains("b.h"),
110+
"expected the added header after invalidating its directory")
111+
}
112+
}
113+
}

‎Tests/SwiftDriverTests/Helpers/TestBuildConfig.swift‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ extension Trait where Self == Testing.ConditionTrait {
184184
return enabled(if: supported, comment ?? "libSwiftScan does not support binary module header dependencies")
185185
}
186186

187+
/// Requires that libSwiftScan supports directory dependencies.
188+
package static func requireScannerSupportsDirectoryDependencies(_ comment: Comment? = nil) -> Self {
189+
let supported = (try? _scannerOracle?.supportsDirectoryDependencies()) ?? false
190+
return enabled(if: supported, comment ?? "libSwiftScan does not support directory dependencies")
191+
}
192+
187193
/// Requires that explicit module verify interface is supported.
188194
package static func requireExplicitModuleVerifyInterface(_ comment: Comment? = nil) -> Self {
189195
let supported = _featureCheckDriver?.isFrontendArgSupported(.inputFileKey) ?? false

0 commit comments

Comments
 (0)