Skip to content

Commit 2639e2e

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 14fedb3 commit 2639e2e

File tree

2 files changed

+97
-4
lines changed

2 files changed

+97
-4
lines changed

Sources/SwiftDriver/Utilities/VirtualPath.swift

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

718+
/// Retrieves the last modification time of the file referenced at the given path.
719+
///
720+
/// If the given file path references a symbolic link, the modification time for the *linked file*
721+
/// - not the symlink itself - is returned.
722+
///
723+
/// - Parameter file: The path to a file.
724+
/// - Throws: `SystemError` if the underlying `stat` operation fails.
725+
/// - Returns: A `Date` value containing the last modification time.
718726
public func lastModificationTime(for file: VirtualPath) throws -> Date {
719727
try resolvingVirtualPath(file) { path in
720728
#if os(macOS)
721729
var s = Darwin.stat()
722-
let err = lstat(path.pathString, &s)
730+
let err = stat(path.pathString, &s)
723731
guard err == 0 else {
724732
throw SystemError.stat(errno, path.pathString)
725733
}
726734
let ti = (TimeInterval(s.st_mtimespec.tv_sec) - kCFAbsoluteTimeIntervalSince1970) + (1.0e-9 * TimeInterval(s.st_mtimespec.tv_nsec))
727735
return Date(timeIntervalSinceReferenceDate: ti)
728736
#else
729-
return try localFileSystem.getFileInfo(file).modTime
737+
// `getFileInfo` is going to ask Foundation to stat this path, and
738+
// Foundation is always going to use `lstat` to do so. This is going to
739+
// do the wrong thing for symbolic links, for which we always want to
740+
// retrieve the mod time of the underlying file. This makes build systems
741+
// that regenerate lots of symlinks but do not otherwise alter the
742+
// contents of files - like Bazel - quite happy.
743+
let path = resolveSymlinks(path)
744+
return try localFileSystem.getFileInfo(path).modTime
730745
#endif
731746
}
732747
}

Tests/SwiftDriverTests/IncrementalCompilationTests.swift

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,22 @@ extension IncrementalCompilationTests {
280280
try checkNullBuild(checkDiagnostics: true)
281281
}
282282
}
283+
284+
func testSymlinkModification() throws {
285+
// Remap
286+
// main.swift -> main.swift~link-target.swift
287+
// other.swift -> other.swift~link-target.swift
288+
for (file, _) in self.inputPathsAndContents {
289+
try localFileSystem.createDirectory(tempDir.appending(component: "links"))
290+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
291+
try localFileSystem.move(from: file, to: linkTarget)
292+
try localFileSystem.removeFileTree(file)
293+
try localFileSystem.createSymbolicLink(file, pointingAt: linkTarget, relative: false)
294+
}
295+
try buildInitialState()
296+
try checkReactionToTouchingSymlinks()
297+
try checkReactionToTouchingSymlinkTargets()
298+
}
283299
}
284300

285301
// MARK: - Test adding an input
@@ -622,6 +638,69 @@ extension IncrementalCompilationTests {
622638
XCTAssert(graph.contains(sourceBasenameWithoutExt: newInput))
623639
XCTAssert(graph.contains(name: topLevelName))
624640
}
641+
642+
/// Check reaction to touching all symlinks
643+
private func checkReactionToTouchingSymlinks(
644+
checkDiagnostics: Bool = false,
645+
extraArguments: [String] = []
646+
) throws {
647+
for (file, _) in self.inputPathsAndContents {
648+
try localFileSystem.removeFileTree(file)
649+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
650+
try localFileSystem.createSymbolicLink(file, pointingAt: linkTarget, relative: false)
651+
}
652+
try doABuild(
653+
"touch both symlinks; non-propagating",
654+
checkDiagnostics: checkDiagnostics,
655+
extraArguments: extraArguments,
656+
expectingRemarks: [
657+
"Enabling incremental cross-module building",
658+
"Incremental compilation: Read dependency graph",
659+
"Incremental compilation: May skip current input {compile: main.o <= main.swift}",
660+
"Incremental compilation: May skip current input {compile: other.o <= other.swift}",
661+
"Incremental compilation: Skipping input: {compile: main.o <= main.swift}",
662+
"Incremental compilation: Skipping input: {compile: other.o <= other.swift}",
663+
"Incremental compilation: Skipping job: Linking theModule",
664+
"Skipped Compiling main.swift",
665+
"Skipped Compiling other.swift",
666+
],
667+
whenAutolinking: autolinkLifecycleExpectations)
668+
}
669+
670+
private func checkReactionToTouchingSymlinkTargets(
671+
checkDiagnostics: Bool = false,
672+
extraArguments: [String] = []
673+
) throws {
674+
for (file, contents) in self.inputPathsAndContents {
675+
let linkTarget = tempDir.appending(component: "links").appending(component: file.basename)
676+
try! localFileSystem.writeFileContents(linkTarget) { $0 <<< contents }
677+
}
678+
try doABuild(
679+
"touch both symlink targets; non-propagating",
680+
checkDiagnostics: checkDiagnostics,
681+
extraArguments: extraArguments,
682+
expectingRemarks: [
683+
"Enabling incremental cross-module building",
684+
"Incremental compilation: Read dependency graph",
685+
"Incremental compilation: Scheduing changed input {compile: main.o <= main.swift}",
686+
"Incremental compilation: Scheduing changed input {compile: other.o <= other.swift}",
687+
"Incremental compilation: Queuing (initial): {compile: main.o <= main.swift}",
688+
"Incremental compilation: Queuing (initial): {compile: other.o <= other.swift}",
689+
"Incremental compilation: not scheduling dependents of main.swift; unknown changes",
690+
"Incremental compilation: not scheduling dependents of other.swift; unknown changes",
691+
"Found 2 batchable jobs",
692+
"Forming into 1 batch",
693+
"Adding {compile: main.swift} to batch 0",
694+
"Adding {compile: other.swift} to batch 0",
695+
"Forming batch job from 2 constituents: main.swift, other.swift",
696+
"Starting Compiling main.swift, other.swift",
697+
"Finished Compiling main.swift, other.swift",
698+
"Incremental compilation: Scheduling all post-compile jobs because something was compiled",
699+
"Starting Linking theModule",
700+
"Finished Linking theModule",
701+
],
702+
whenAutolinking: autolinkLifecycleExpectations)
703+
}
625704
}
626705

627706
// MARK: - Incremental test perturbation helpers
@@ -776,8 +855,7 @@ extension IncrementalCompilationTests {
776855
}
777856
}
778857

779-
private func doABuildWithoutExpectations( arguments: [String]
780-
) throws -> Driver {
858+
private func doABuildWithoutExpectations(arguments: [String]) throws -> Driver {
781859
// If not checking, print out the diagnostics
782860
let diagnosticEngine = DiagnosticsEngine(handlers: [
783861
{print($0, to: &stderrStream); stderrStream.flush()}

0 commit comments

Comments
 (0)