Skip to content

Commit 5ee692c

Browse files
committed
Use stat Instead of lstat
Using `lstat` here will do the wrong thing when the input path is a symlink since it's going to give us mod time info on the symlink instead of the file it references. Since mod time updates are intentionally not propagated across links, this has the potential to cause us to miscompile if a file's contents change on disk but the symlink passed as input hasn't actually changed. It also has the potential to defeat the incremental build when build systems that generate symlinks on every build interact with the driver. On Darwin (and, technically, other UNIX-likes) the fix is simple: use stat instead of lstat which will resolve symlinks for us. On Windows the answer is... complicated. Instead, on the fallback path, resolve symlinks before retrieving the modification time of the file. rdar://78828871
1 parent f2a4eec commit 5ee692c

File tree

2 files changed

+95
-2
lines changed

2 files changed

+95
-2
lines changed

Sources/SwiftDriver/Utilities/VirtualPath.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -721,18 +721,33 @@ extension TSCBasic.FileSystem {
721721
try resolvingVirtualPath(path, apply: exists)
722722
}
723723

724+
/// Retrieves the last modification time of the file referenced at the given path.
725+
///
726+
/// If the given file path references a symbolic link, the modification time for the *linked file*
727+
/// - not the symlink itself - is returned.
728+
///
729+
/// - Parameter file: The path to a file.
730+
/// - Throws: `SystemError` if the underlying `stat` operation fails.
731+
/// - Returns: A `Date` value containing the last modification time.
724732
public func lastModificationTime(for file: VirtualPath) throws -> Date {
725733
try resolvingVirtualPath(file) { path in
726734
#if os(macOS)
727735
var s = Darwin.stat()
728-
let err = lstat(path.pathString, &s)
736+
let err = stat(path.pathString, &s)
729737
guard err == 0 else {
730738
throw SystemError.stat(errno, path.pathString)
731739
}
732740
let ti = (TimeInterval(s.st_mtimespec.tv_sec) - kCFAbsoluteTimeIntervalSince1970) + (1.0e-9 * TimeInterval(s.st_mtimespec.tv_nsec))
733741
return Date(timeIntervalSinceReferenceDate: ti)
734742
#else
735-
return try localFileSystem.getFileInfo(file).modTime
743+
// `getFileInfo` is going to ask Foundation to stat this path, and
744+
// Foundation is always going to use `lstat` to do so. This is going to
745+
// do the wrong thing for symbolic links, for which we always want to
746+
// retrieve the mod time of the underlying file. This makes build systems
747+
// that regenerate lots of symlinks but do not otherwise alter the
748+
// contents of files - like Bazel - quite happy.
749+
let path = resolveSymlinks(path)
750+
return try localFileSystem.getFileInfo(path).modTime
736751
#endif
737752
}
738753
}

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,22 @@ extension IncrementalCompilationTests {
285285
try checkNullBuild(checkDiagnostics: true)
286286
}
287287
}
288+
289+
func testSymlinkModification() throws {
290+
// Remap
291+
// main.swift -> links/main.swift
292+
// other.swift -> links/other.swift
293+
for (file, _) in self.inputPathsAndContents {
294+
try localFileSystem.createDirectory(tempDir.appending(component: "links"))
295+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
296+
try localFileSystem.move(from: file, to: linkTarget)
297+
try localFileSystem.removeFileTree(file)
298+
try localFileSystem.createSymbolicLink(file, pointingAt: linkTarget, relative: false)
299+
}
300+
try buildInitialState()
301+
try checkReactionToTouchingSymlinks(checkDiagnostics: true)
302+
try checkReactionToTouchingSymlinkTargets(checkDiagnostics: true)
303+
}
288304
}
289305

290306
// MARK: - Test adding an input
@@ -826,6 +842,68 @@ extension IncrementalCompilationTests {
826842

827843
return graph
828844
}
845+
846+
private func checkReactionToTouchingSymlinks(
847+
checkDiagnostics: Bool = false,
848+
extraArguments: [String] = []
849+
) throws {
850+
for (file, _) in self.inputPathsAndContents {
851+
try localFileSystem.removeFileTree(file)
852+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
853+
try localFileSystem.createSymbolicLink(file, pointingAt: linkTarget, relative: false)
854+
}
855+
try doABuild(
856+
"touch both symlinks; non-propagating",
857+
checkDiagnostics: checkDiagnostics,
858+
extraArguments: extraArguments,
859+
expectingRemarks: [
860+
"Enabling incremental cross-module building",
861+
"Incremental compilation: Read dependency graph",
862+
"Incremental compilation: May skip current input: {compile: main.o <= main.swift}",
863+
"Incremental compilation: May skip current input: {compile: other.o <= other.swift}",
864+
"Incremental compilation: Skipping input: {compile: main.o <= main.swift}",
865+
"Incremental compilation: Skipping input: {compile: other.o <= other.swift}",
866+
"Incremental compilation: Skipping job: Linking theModule",
867+
"Skipped Compiling main.swift",
868+
"Skipped Compiling other.swift",
869+
],
870+
whenAutolinking: autolinkLifecycleExpectations)
871+
}
872+
873+
private func checkReactionToTouchingSymlinkTargets(
874+
checkDiagnostics: Bool = false,
875+
extraArguments: [String] = []
876+
) throws {
877+
for (file, contents) in self.inputPathsAndContents {
878+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
879+
try! localFileSystem.writeFileContents(linkTarget) { $0 <<< contents }
880+
}
881+
try doABuild(
882+
"touch both symlink targets; non-propagating",
883+
checkDiagnostics: checkDiagnostics,
884+
extraArguments: extraArguments,
885+
expectingRemarks: [
886+
"Enabling incremental cross-module building",
887+
"Incremental compilation: Read dependency graph",
888+
"Incremental compilation: Scheduing changed input {compile: main.o <= main.swift}",
889+
"Incremental compilation: Scheduing changed input {compile: other.o <= other.swift}",
890+
"Incremental compilation: Queuing (initial): {compile: main.o <= main.swift}",
891+
"Incremental compilation: Queuing (initial): {compile: other.o <= other.swift}",
892+
"Incremental compilation: not scheduling dependents of main.swift; unknown changes",
893+
"Incremental compilation: not scheduling dependents of other.swift; unknown changes",
894+
"Found 2 batchable jobs",
895+
"Forming into 1 batch",
896+
"Adding {compile: main.swift} to batch 0",
897+
"Adding {compile: other.swift} to batch 0",
898+
"Forming batch job from 2 constituents: main.swift, other.swift",
899+
"Starting Compiling main.swift, other.swift",
900+
"Finished Compiling main.swift, other.swift",
901+
"Incremental compilation: Scheduling all post-compile jobs because something was compiled",
902+
"Starting Linking theModule",
903+
"Finished Linking theModule",
904+
],
905+
whenAutolinking: autolinkLifecycleExpectations)
906+
}
829907
}
830908

831909
// MARK: - Incremental test perturbation helpers

0 commit comments

Comments
 (0)