Skip to content

Commit e31e7db

Browse files
committed
Fix path handling for plugins on Windows
Due to RFC8089 compliance changes for Foundation.URL in Swift 6, URL.path does _NOT_ behave as one might expect, producing a path with a leading slash which will be interpreted by Windows as relative. Closes #6851
1 parent df04096 commit e31e7db

9 files changed

+120
-31
lines changed

Sources/Basics/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ add_library(Basics
7272
SQLiteBackedCache.swift
7373
TestingLibrary.swift
7474
Triple+Basics.swift
75+
URL.swift
7576
Version+Extensions.swift
7677
WritableByteStream+Extensions.swift
7778
Vendor/Triple.swift

Sources/Basics/URL.swift

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift open source project
4+
//
5+
// Copyright (c) 2021 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See http://swift.org/LICENSE.txt for license information
9+
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import struct Foundation.URL
14+
15+
extension URL {
16+
/// Returns the path of the file URL.
17+
///
18+
/// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash.
19+
///
20+
/// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable.
21+
public var filePath: AbsolutePath {
22+
get throws {
23+
guard isFileURL else {
24+
throw FileURLError.notRepresentable(self)
25+
}
26+
return try withUnsafeFileSystemRepresentation { cString in
27+
guard let cString else {
28+
throw FileURLError.notRepresentable(self)
29+
}
30+
return try AbsolutePath(validating: String(cString: cString))
31+
}
32+
}
33+
}
34+
}
35+
36+
fileprivate enum FileURLError: Error {
37+
case notRepresentable(URL)
38+
}

Sources/PackagePlugin/Context.swift

+5-4
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,18 @@ public struct PluginContext {
6363
if let triples = tool.triples, triples.isEmpty {
6464
throw PluginContextError.toolNotSupportedOnTargetPlatform(name: name)
6565
}
66-
return Tool(name: name, path: Path(url: tool.path), url: tool.path)
66+
return try Tool(name: name, path: Path(url: tool.path), url: tool.path)
6767
} else {
6868
for dir in self.toolSearchDirectoryURLs {
6969
#if os(Windows)
7070
let hostExecutableSuffix = ".exe"
7171
#else
7272
let hostExecutableSuffix = ""
7373
#endif
74-
let path = dir.appendingPathComponent(name + hostExecutableSuffix)
75-
if FileManager.default.isExecutableFile(atPath: path.path) {
76-
return Tool(name: name, path: Path(url: path), url: path)
74+
let pathURL = dir.appendingPathComponent(name + hostExecutableSuffix)
75+
let path = try Path(url: pathURL)
76+
if FileManager.default.isExecutableFile(atPath: path.stringValue) {
77+
return Tool(name: name, path: path, url: pathURL)
7778
}
7879
}
7980
}

Sources/PackagePlugin/PackageManagerProxy.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public struct PackageManager {
113113
/// Full path of the built artifact in the local file system.
114114
@available(_PackageDescription, deprecated: 6.0, renamed: "url")
115115
public var path: Path {
116-
Path(url: self.url)
116+
try! Path(url: self.url)
117117
}
118118

119119
/// Full path of the built artifact in the local file system.
@@ -187,7 +187,7 @@ public struct PackageManager {
187187
/// `llvm-cov`, if `enableCodeCoverage` was set in the test parameters.
188188
@available(_PackageDescription, deprecated: 6.0, renamed: "codeCoverageDataFileURL")
189189
public var codeCoverageDataFile: Path? {
190-
self.codeCoverageDataFileURL.map { Path(url: $0) }
190+
self.codeCoverageDataFileURL.map { try! Path(url: $0) }
191191
}
192192

193193
/// Path of a generated `.profdata` file suitable for processing using
@@ -275,7 +275,7 @@ public struct PackageManager {
275275
/// The directory that contains the symbol graph files for the target.
276276
@available(_PackageDescription, deprecated: 6.0, renamed: "directoryURL")
277277
public var directoryPath: Path {
278-
Path(url: self.directoryURL)
278+
try! Path(url: self.directoryURL)
279279
}
280280

281281
/// The directory that contains the symbol graph files for the target.

Sources/PackagePlugin/Path.swift

+27-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public struct Path: Hashable {
2323
self._string = string
2424
}
2525

26-
init(url: URL) {
27-
self._string = url.path
26+
init(url: URL) throws {
27+
self._string = try url.filePath
2828
}
2929

3030
/// A string representation of the path.
@@ -157,3 +157,28 @@ extension String.StringInterpolation {
157157
self.appendInterpolation(path.string)
158158
}
159159
}
160+
161+
extension URL {
162+
/// Returns the path of the file URL.
163+
///
164+
/// This should always be used whenever the file path equivalent of a URL is needed. DO NOT use ``path`` or ``path(percentEncoded:)``, as these deal in terms of the path portion of the URL representation per RFC8089, which on Windows would include a leading slash.
165+
///
166+
/// - throws: ``FileURLError`` if the URL does not represent a file or its path is otherwise not representable.
167+
fileprivate var filePath: String {
168+
get throws {
169+
guard isFileURL else {
170+
throw FileURLError.notRepresentable(self)
171+
}
172+
return try withUnsafeFileSystemRepresentation { cString in
173+
guard let cString else {
174+
throw FileURLError.notRepresentable(self)
175+
}
176+
return String(cString: cString)
177+
}
178+
}
179+
}
180+
}
181+
182+
fileprivate enum FileURLError: Error {
183+
case notRepresentable(URL)
184+
}

Sources/PackagePlugin/Plugin.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,12 @@ extension Plugin {
158158
return (path, tool.triples)
159159
}
160160

161-
context = PluginContext(
161+
context = try PluginContext(
162162
package: package,
163163
pluginWorkDirectory: Path(url: pluginWorkDirectory),
164164
pluginWorkDirectoryURL: pluginWorkDirectory,
165165
accessibleTools: accessibleTools,
166-
toolSearchDirectories: toolSearchDirectories.map { Path(url: $0) },
166+
toolSearchDirectories: toolSearchDirectories.map { try Path(url: $0) },
167167
toolSearchDirectoryURLs: toolSearchDirectories)
168168

169169
let pluginGeneratedSources = try generatedSources.map { try deserializer.url(for: $0) }
@@ -248,12 +248,12 @@ extension Plugin {
248248
let path = try deserializer.url(for: tool.path)
249249
return (path, tool.triples)
250250
}
251-
context = PluginContext(
251+
context = try PluginContext(
252252
package: package,
253253
pluginWorkDirectory: Path(url: pluginWorkDirectory),
254254
pluginWorkDirectoryURL: pluginWorkDirectory,
255255
accessibleTools: accessibleTools,
256-
toolSearchDirectories: toolSearchDirectories.map { Path(url: $0) },
256+
toolSearchDirectories: toolSearchDirectories.map { try Path(url: $0) },
257257
toolSearchDirectoryURLs: toolSearchDirectories)
258258
}
259259
catch {

Sources/PackagePlugin/PluginContextDeserializer.swift

+20-10
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,18 @@ internal struct PluginContextDeserializer {
4242

4343
// Compose a path based on an optional base path and a subpath.
4444
let wirePath = wireInput.paths[id]
45-
let basePath = try wireInput.paths[id].baseURLId.map{ try self.url(for: $0) } ?? URL(fileURLWithPath: "/")
46-
let path = basePath.appendingPathComponent(wirePath.subpath)
45+
let basePath = try wireInput.paths[id].baseURLId.map{ try self.url(for: $0) }
46+
let path: URL
47+
if let basePath {
48+
path = basePath.appendingPathComponent(wirePath.subpath)
49+
} else {
50+
#if os(Windows)
51+
// Windows does not have a single root path like UNIX, if this component has no base path, it IS the root and should not be joined with anything
52+
path = URL(fileURLWithPath: wirePath.subpath)
53+
#else
54+
path = URL(fileURLWithPath: "/").appendingPathComponent(wirePath.subpath)
55+
#endif
56+
}
4757

4858
// Store it for the next look up.
4959
urlsById[id] = path
@@ -92,9 +102,9 @@ internal struct PluginContextDeserializer {
92102
case .unknown:
93103
type = .unknown
94104
}
95-
return File(path: Path(url: path), url: path, type: type)
105+
return try File(path: Path(url: path), url: path, type: type)
96106
})
97-
target = SwiftSourceModuleTarget(
107+
target = try SwiftSourceModuleTarget(
98108
id: String(id),
99109
name: wireTarget.name,
100110
kind: .init(kind),
@@ -125,9 +135,9 @@ internal struct PluginContextDeserializer {
125135
case .unknown:
126136
type = .unknown
127137
}
128-
return File(path: Path(url: path), url: path, type: type)
138+
return try File(path: Path(url: path), url: path, type: type)
129139
})
130-
target = ClangSourceModuleTarget(
140+
target = try ClangSourceModuleTarget(
131141
id: String(id),
132142
name: wireTarget.name,
133143
kind: .init(kind),
@@ -138,7 +148,7 @@ internal struct PluginContextDeserializer {
138148
sourceFiles: sourceFiles,
139149
preprocessorDefinitions: preprocessorDefinitions,
140150
headerSearchPaths: headerSearchPaths,
141-
publicHeadersDirectory: publicHeadersDir.map { .init(url: $0) },
151+
publicHeadersDirectory: publicHeadersDir.map { try .init(url: $0) },
142152
publicHeadersDirectoryURL: publicHeadersDir,
143153
linkedLibraries: linkedLibraries,
144154
linkedFrameworks: linkedFrameworks,
@@ -162,7 +172,7 @@ internal struct PluginContextDeserializer {
162172
case .remote(let url):
163173
artifactOrigin = .remote(url: url)
164174
}
165-
target = BinaryArtifactTarget(
175+
target = try BinaryArtifactTarget(
166176
id: String(id),
167177
name: wireTarget.name,
168178
directory: Path(url: directory),
@@ -174,7 +184,7 @@ internal struct PluginContextDeserializer {
174184
artifactURL: artifact)
175185

176186
case let .systemLibraryInfo(pkgConfig, compilerFlags, linkerFlags):
177-
target = SystemLibraryTarget(
187+
target = try SystemLibraryTarget(
178188
id: String(id),
179189
name: wireTarget.name,
180190
directory: Path(url: directory),
@@ -261,7 +271,7 @@ internal struct PluginContextDeserializer {
261271
case .registry(let identity, let displayVersion):
262272
.registry(identity: identity, displayVersion: displayVersion)
263273
}
264-
let package = Package(
274+
let package = try Package(
265275
id: wirePackage.identity,
266276
displayName: wirePackage.displayName,
267277
directory: Path(url: directory),

Sources/SPMBuildCore/Plugins/PluginContextSerializer.swift

+15-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,21 @@ internal struct PluginContextSerializer {
4343

4444
// Split up the path into a base path and a subpath (currently always with the last path component as the
4545
// subpath, but this can be optimized where there are sequences of path components with a valence of one).
46-
let basePathId = (path.parentDirectory.isRoot ? nil : try serialize(path: path.parentDirectory))
46+
let basePathId: Int?
47+
if path.parentDirectory.isRoot {
48+
// Windows does not have a single root path like UNIX, so capture the root path itself such that we can rejoin it later
49+
#if os(Windows)
50+
let id = paths.count
51+
paths.append(.init(baseURLId: nil, subpath: path.parentDirectory.pathString))
52+
pathsToIds[path] = id
53+
basePathId = id
54+
#else
55+
basePathId = nil
56+
#endif
57+
} else {
58+
basePathId = try serialize(path: path.parentDirectory)
59+
}
60+
4761
let subpathString = path.basename
4862

4963
// Finally assign the next wire ID to the path, and append a serialized Path record.

Sources/SPMBuildCore/Plugins/PluginInvocation.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -272,24 +272,24 @@ extension PluginModule {
272272
}
273273
self.invocationDelegate.pluginDefinedBuildCommand(
274274
displayName: config.displayName,
275-
executable: try AbsolutePath(validating: config.executable.path),
275+
executable: try config.executable.filePath,
276276
arguments: config.arguments,
277277
environment: config.environment,
278-
workingDirectory: try config.workingDirectory.map{ try AbsolutePath(validating: $0.path) },
279-
inputFiles: try inputFiles.map{ try AbsolutePath(validating: $0.path) },
280-
outputFiles: try outputFiles.map{ try AbsolutePath(validating: $0.path) })
278+
workingDirectory: try config.workingDirectory.map{ try $0.filePath },
279+
inputFiles: try inputFiles.map{ try $0.filePath },
280+
outputFiles: try outputFiles.map{ try $0.filePath })
281281

282282
case .definePrebuildCommand(let config, let outputFilesDir):
283283
if config.version != 2 {
284284
throw PluginEvaluationError.pluginUsesIncompatibleVersion(expected: 2, actual: config.version)
285285
}
286286
let success = self.invocationDelegate.pluginDefinedPrebuildCommand(
287287
displayName: config.displayName,
288-
executable: try AbsolutePath(validating: config.executable.path),
288+
executable: try config.executable.filePath,
289289
arguments: config.arguments,
290290
environment: config.environment,
291-
workingDirectory: try config.workingDirectory.map{ try AbsolutePath(validating: $0.path) },
292-
outputFilesDirectory: try AbsolutePath(validating: outputFilesDir.path))
291+
workingDirectory: try config.workingDirectory.map{ try $0.filePath },
292+
outputFilesDirectory: try outputFilesDir.filePath)
293293

294294
if !success {
295295
exitEarly = true

0 commit comments

Comments
 (0)