Skip to content

Commit 4f60486

Browse files
committed
Fix realpath to do the right thing on all platforms
This regressed as part of #427, which attempted to simplify API calls in FSProxy to use Foundation rather than bespoke implementations going directly to Win32/POSIX API. However, `standardizingPath` on Windows doesn't actually handle 8.3 filenames correctly, and so Swift Build doesn't know (for example) that C:\Users\JAKEPE~1 and C:\Users\jakepetroules are the same path. Use the canonicalPathKey from URLResourceValues to canonicalize the path appropriately on both platforms, which under the hood uses GetFinalPathNameByHandleW on Windows and realpath on POSIX, matching the previous behavior. With this change, the androidCommandLineTool test now passes on Windows, which was previously failing due to differences between paths in 8.3 form vs long form.
1 parent f9217dc commit 4f60486

File tree

4 files changed

+82
-41
lines changed

4 files changed

+82
-41
lines changed

Sources/SWBUtil/FSProxy.swift

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -757,16 +757,15 @@ class LocalFS: FSProxy, @unchecked Sendable {
757757
}
758758

759759
func realpath(_ path: Path) throws -> Path {
760-
#if os(Windows)
761-
guard exists(path) else {
760+
if path.isAbsolute && !exists(path) {
762761
throw POSIXError(ENOENT, context: "realpath", path.str)
763762
}
764-
return Path(path.str.standardizingPath)
765-
#else
766-
guard let result = SWBLibc.realpath(path.str, nil) else { throw POSIXError(errno, context: "realpath", path.str) }
767-
defer { free(result) }
768-
return Path(String(cString: result))
769-
#endif
763+
let url = URL(fileURLWithPath: path.str)
764+
let values = try url.resolvingSymlinksInPath().resourceValues(forKeys: Set([URLResourceKey.canonicalPathKey]))
765+
guard let canonicalPath = values.canonicalPath else {
766+
throw POSIXError(ENOENT, context: "realpath", path.str)
767+
}
768+
return try Path(canonicalPath.canonicalPathRepresentation)
770769
}
771770

772771
func isOnPotentiallyRemoteFileSystem(_ path: Path) -> Bool {

Sources/SWBUtil/Path.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,39 @@ extension Path {
927927
}
928928
}
929929

930+
extension String {
931+
@_spi(Testing) public var canonicalPathRepresentation: String {
932+
get throws {
933+
#if os(Windows)
934+
return try withCString(encodedAs: UTF16.self) { platformPath in
935+
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
936+
return String(decodingCString: canonicalPath, as: UTF16.self)
937+
}
938+
}
939+
#else
940+
return self
941+
#endif
942+
}
943+
}
944+
}
945+
946+
extension Path {
947+
@_spi(Testing) public var canonicalPathRepresentation: String {
948+
get throws {
949+
#if os(Windows)
950+
return try withPlatformString { platformPath in
951+
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
952+
return String(decodingCString: canonicalPath, as: UTF16.self)
953+
}
954+
}
955+
#else
956+
return str
957+
#endif
958+
}
959+
}
960+
}
961+
962+
930963
/// A wrapper for a string which is used to identify an absolute path on the file system.
931964
public struct AbsolutePath: Hashable, Equatable, Serializable, Sendable {
932965
public let path: Path

Tests/SWBUtilTests/FSProxyTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@ import SWBLibc
1717
import SWBTestSupport
1818
@_spi(TestSupport) import SWBUtil
1919

20+
#if canImport(System)
21+
public import System
22+
#else
23+
public import SystemPackage
24+
#endif
25+
2026
@Suite fileprivate struct FSProxyTests {
2127

2228
#if !os(Windows)
@@ -1333,6 +1339,41 @@ import SWBTestSupport
13331339
#expect(try fs.read(dir.join("foo")) == ByteString(encodingAsUTF8: "a"))
13341340
}
13351341
}
1342+
1343+
@Test(.requireHostOS(.windows))
1344+
func realpathWindows() async throws {
1345+
let fs = localFS
1346+
let windir = try #require(getEnvironmentVariable("WINDIR"))
1347+
do {
1348+
// Case-insensitive comparison because WINDIR might be C:\WINDOWS while the actual path is C:\Windows
1349+
// The main thing is the \\?\ prefix handling
1350+
#expect(try fs.realpath(Path(windir)).str.caseInsensitiveCompare(windir) == .orderedSame)
1351+
#expect(try fs.realpath(Path(#"\\?\"# + windir)).str.caseInsensitiveCompare(windir) == .orderedSame)
1352+
}
1353+
1354+
do {
1355+
let root = Path(windir).drive
1356+
#expect(try fs.realpath(root.join("Program Files")).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame)
1357+
1358+
if !fs.exists(root.join("Progra~1")) {
1359+
withKnownIssue {
1360+
Issue.record("8.3 filenames are likely disabled in this environment (running in a container?)")
1361+
}
1362+
return
1363+
}
1364+
1365+
#expect(try fs.realpath(root.join("Progra~1")).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame)
1366+
#expect(try fs.realpath(Path(#"\\?\"# + root.join("Progra~1").str)).str.caseInsensitiveCompare(root.join("Program Files").str) == .orderedSame)
1367+
}
1368+
}
1369+
}
1370+
1371+
fileprivate extension Path {
1372+
var drive: Path {
1373+
var fp = FilePath(str)
1374+
fp.components.removeAll()
1375+
return Path(fp.string).withTrailingSlash
1376+
}
13361377
}
13371378

13381379
/// Helper method to test file tree removal method on the given file system.

Tests/SWBUtilTests/PathWindowsTests.swift

Lines changed: 1 addition & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import Testing
1414
import SWBTestSupport
15-
import SWBUtil
15+
@_spi(Testing) import SWBUtil
1616

1717
@Suite(.requireHostOS(.windows))
1818
fileprivate struct PathWindowsTests {
@@ -58,35 +58,3 @@ fileprivate struct PathWindowsTests {
5858
#expect(try Path(current.str.prefix(2) + String(repeating: "foo/bar/baz/", count: 22)).canonicalPathRepresentation == "\\\\?\\" + current.join(String(repeating: "\\foo\\bar\\baz", count: 22)).str)
5959
}
6060
}
61-
62-
fileprivate extension String {
63-
var canonicalPathRepresentation: String {
64-
get throws {
65-
#if os(Windows)
66-
return try withCString(encodedAs: UTF16.self) { platformPath in
67-
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
68-
return String(decodingCString: canonicalPath, as: UTF16.self)
69-
}
70-
}
71-
#else
72-
return self
73-
#endif
74-
}
75-
}
76-
}
77-
78-
fileprivate extension Path {
79-
var canonicalPathRepresentation: String {
80-
get throws {
81-
#if os(Windows)
82-
return try withPlatformString { platformPath in
83-
return try platformPath.withCanonicalPathRepresentation { canonicalPath in
84-
return String(decodingCString: canonicalPath, as: UTF16.self)
85-
}
86-
}
87-
#else
88-
return str
89-
#endif
90-
}
91-
}
92-
}

0 commit comments

Comments
 (0)