Skip to content

Commit 7b686fd

Browse files
committed
Improve path management and filesystem operation ergonomics
Use FilePath instead of file URL's. FilePath is recommended as the system data type to be used to represent local file paths for command-line tools. The methods have simpler names while retaining the same vital path arithmetic functions. It is much less likely that a stringer will accidentally print out a file URL to the user when a path is intended. Remove usage of 'FileManager.default' in favour of API's that are far less verbose to type and read. Make top-level API functions for operations, such as checking if a file exists, removing files, moving them, copying them, etc. Once SwiftlyCore is imported then these functions become available for use. These functions accept FilePath, not URL or String for a measure of type safety. Make the new API's async by default to permit swapping FileManager with another implementation that has async operations. The most common file path operation is appending. Make use of operator overloading to make these operations much cleaner, and clearer with the division operator.
1 parent 5645873 commit 7b686fd

30 files changed

+1003
-870
lines changed

Package.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ let package = Package(
2626
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"),
2727
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.6.0"),
2828
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.7.0"),
29+
.package(url: "https://github.com/apple/swift-system", from: "1.4.2"),
2930
// This dependency provides the correct version of the formatter so that you can run `swift run swiftformat Package.swift Plugins/ Sources/ Tests/`
3031
.package(url: "https://github.com/nicklockwood/SwiftFormat", exact: "0.49.18"),
3132
],
@@ -38,6 +39,7 @@ let package = Package(
3839
.target(name: "LinuxPlatform", condition: .when(platforms: [.linux])),
3940
.target(name: "MacOSPlatform", condition: .when(platforms: [.macOS])),
4041
.product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"),
42+
.product(name: "SystemPackage", package: "swift-system"),
4143
]
4244
),
4345
.executableTarget(
@@ -58,6 +60,7 @@ let package = Package(
5860
.product(name: "NIOFoundationCompat", package: "swift-nio"),
5961
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
6062
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
63+
.product(name: "SystemPackage", package: "swift-system"),
6164
],
6265
),
6366
.target(
@@ -114,6 +117,7 @@ let package = Package(
114117
dependencies: [
115118
"SwiftlyCore",
116119
"CLibArchive",
120+
.product(name: "SystemPackage", package: "swift-system"),
117121
],
118122
linkerSettings: [
119123
.linkedLibrary("z"),
@@ -123,6 +127,7 @@ let package = Package(
123127
name: "MacOSPlatform",
124128
dependencies: [
125129
"SwiftlyCore",
130+
.product(name: "SystemPackage", package: "swift-system"),
126131
]
127132
),
128133
.systemLibrary(
@@ -134,7 +139,10 @@ let package = Package(
134139
),
135140
.testTarget(
136141
name: "SwiftlyTests",
137-
dependencies: ["Swiftly"],
142+
dependencies: [
143+
"Swiftly",
144+
.product(name: "SystemPackage", package: "swift-system"),
145+
],
138146
resources: [
139147
.embedInCode("mock-signing-key-private.pgp"),
140148
]

Sources/LinuxPlatform/Extract.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CLibArchive
22
import Foundation
3+
import SystemPackage
34

45
// The code in this file consists mainly of a Swift port of the "Complete Extractor" example included in the libarchive
56
// documentation: https://github.com/libarchive/libarchive/wiki/Examples#a-complete-extractor
@@ -44,7 +45,7 @@ func copyData(readArchive: OpaquePointer?, writeArchive: OpaquePointer?) throws
4445
/// the provided closure which will return the path the file will be written to.
4546
///
4647
/// This uses libarchive under the hood, so a wide variety of archive formats are supported (e.g. .tar.gz).
47-
func extractArchive(atPath archivePath: URL, transform: (String) -> URL) throws {
48+
func extractArchive(atPath archivePath: FilePath, transform: (String) -> FilePath) throws {
4849
var flags = Int32(0)
4950
flags = ARCHIVE_EXTRACT_TIME
5051
flags |= ARCHIVE_EXTRACT_PERM
@@ -66,8 +67,8 @@ func extractArchive(atPath archivePath: URL, transform: (String) -> URL) throws
6667
archive_write_free(ext)
6768
}
6869

69-
if archive_read_open_filename(a, archivePath.path, 10240) != 0 {
70-
throw ExtractError(message: "Failed to open \"\(archivePath.path)\"")
70+
if archive_read_open_filename(a, archivePath.string, 10240) != 0 {
71+
throw ExtractError(message: "Failed to open \"\(archivePath)\"")
7172
}
7273

7374
while true {
@@ -82,7 +83,7 @@ func extractArchive(atPath archivePath: URL, transform: (String) -> URL) throws
8283
}
8384

8485
let currentPath = String(cString: archive_entry_pathname(entry))
85-
archive_entry_set_pathname(entry, transform(currentPath).path)
86+
archive_entry_set_pathname(entry, transform(currentPath).string)
8687
r = archive_write_header(ext, entry)
8788
guard r == ARCHIVE_OK else {
8889
throw ExtractError(archive: ext)

Sources/LinuxPlatform/Linux.swift

Lines changed: 85 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import SwiftlyCore
3+
import SystemPackage
34

45
/// `Platform` implementation for Linux systems.
56
/// This implementation can be reused for any supported Linux platform.
@@ -18,24 +19,22 @@ public struct Linux: Platform {
1819

1920
public init() {}
2021

21-
public var defaultSwiftlyHomeDirectory: URL {
22+
public var defaultSwiftlyHomeDir: FilePath {
2223
if let dir = ProcessInfo.processInfo.environment["XDG_DATA_HOME"] {
23-
return URL(fileURLWithPath: dir).appendingPathComponent("swiftly", isDirectory: true)
24+
return FilePath(dir) / "swiftly"
2425
} else {
25-
return FileManager.default.homeDirectoryForCurrentUser
26-
.appendingPathComponent(".local/share/swiftly", isDirectory: true)
26+
return homeDir / ".local/share/swiftly"
2727
}
2828
}
2929

30-
public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL {
31-
ctx.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) }
32-
?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) }
33-
?? FileManager.default.homeDirectoryForCurrentUser
34-
.appendingPathComponent(".local/share/swiftly/bin", isDirectory: true)
30+
public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> FilePath {
31+
ctx.mockedHomeDir.map { $0 / "bin" }
32+
?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { FilePath($0) }
33+
?? homeDir / ".local/share/swiftly/bin"
3534
}
3635

37-
public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL {
38-
self.swiftlyHomeDir(ctx).appendingPathComponent("toolchains", isDirectory: true)
36+
public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> FilePath {
37+
self.swiftlyHomeDir(ctx) / "toolchains"
3938
}
4039

4140
public var toolchainFileExtension: String {
@@ -45,12 +44,12 @@ public struct Linux: Platform {
4544
private static let skipVerificationMessage: String =
4645
"To skip signature verification, specify the --no-verify flag."
4746

48-
public func verifySwiftlySystemPrerequisites() throws {
47+
public func verifySwiftlySystemPrerequisites() async throws {
4948
// Check if the root CA certificates are installed on this system for NIOSSL to use.
5049
// This list comes from LinuxCABundle.swift in NIOSSL.
5150
var foundTrustedCAs = false
5251
for crtFile in ["/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt"] {
53-
if URL(fileURLWithPath: crtFile).fileExists() {
52+
if try await fileExists(atPath: FilePath(crtFile)) {
5453
foundTrustedCAs = true
5554
break
5655
}
@@ -267,21 +266,17 @@ public struct Linux: Platform {
267266
}
268267

269268
let tmpFile = self.getTempFilePath()
270-
let _ = FileManager.default.createFile(
271-
atPath: tmpFile.path, contents: nil, attributes: [.posixPermissions: 0o600]
272-
)
273-
defer {
274-
try? FileManager.default.removeItem(at: tmpFile)
275-
}
276-
277-
try await ctx.httpClient.getGpgKeys().download(to: tmpFile)
278-
if let mockedHomeDir = ctx.mockedHomeDir {
279-
try self.runProgram(
280-
"gpg", "--import", tmpFile.path, quiet: true,
281-
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
282-
)
283-
} else {
284-
try self.runProgram("gpg", "--import", tmpFile.path, quiet: true)
269+
try await create(file: tmpFile, contents: nil, mode: 0o600)
270+
try await withTemporary(files: tmpFile) {
271+
try await ctx.httpClient.getGpgKeys().download(to: tmpFile)
272+
if let mockedHomeDir = ctx.mockedHomeDir {
273+
try self.runProgram(
274+
"gpg", "--import", "\(tmpFile)", quiet: true,
275+
env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string]
276+
)
277+
} else {
278+
try self.runProgram("gpg", "--import", "\(tmpFile)", quiet: true)
279+
}
285280
}
286281
}
287282

@@ -333,69 +328,63 @@ public struct Linux: Platform {
333328
}
334329

335330
public func install(
336-
_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool
331+
_ ctx: SwiftlyCoreContext, from tmpFile: FilePath, version: ToolchainVersion, verbose: Bool
337332
) async throws {
338-
guard tmpFile.fileExists() else {
333+
guard try await fileExists(atPath: tmpFile) else {
339334
throw SwiftlyError(message: "\(tmpFile) doesn't exist")
340335
}
341336

342-
if !self.swiftlyToolchainsDir(ctx).fileExists() {
343-
try FileManager.default.createDirectory(
344-
at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false
345-
)
337+
if !(try await fileExists(atPath: self.swiftlyToolchainsDir(ctx))) {
338+
try await mkdir(atPath: self.swiftlyToolchainsDir(ctx))
346339
}
347340

348341
await ctx.print("Extracting toolchain...")
349-
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(version.name)
342+
let toolchainDir = self.swiftlyToolchainsDir(ctx) / version.name
350343

351-
if toolchainDir.fileExists() {
352-
try FileManager.default.removeItem(at: toolchainDir)
344+
if try await fileExists(atPath: toolchainDir) {
345+
try await remove(atPath: toolchainDir)
353346
}
354347

355348
try extractArchive(atPath: tmpFile) { name in
356349
// drop swift-a.b.c-RELEASE etc name from the extracted files.
357350
let relativePath = name.drop { c in c != "/" }.dropFirst()
358351

359352
// prepend /path/to/swiftlyHomeDir/toolchains/<toolchain> to each file name
360-
let destination = toolchainDir.appendingPathComponent(String(relativePath))
353+
let destination = toolchainDir / String(relativePath)
361354

362355
if verbose {
363356
// To avoid having to make extractArchive async this is a regular print
364357
// to stdout. Note that it is unlikely that the test mocking will require
365358
// capturing this output.
366-
print("\(destination.path)")
359+
print("\(destination)")
367360
}
368361

369362
// prepend /path/to/swiftlyHomeDir/toolchains/<toolchain> to each file name
370363
return destination
371364
}
372365
}
373366

374-
public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) async throws {
375-
guard archive.fileExists() else {
367+
public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: FilePath) async throws {
368+
guard try await fileExists(atPath: archive) else {
376369
throw SwiftlyError(message: "\(archive) doesn't exist")
377370
}
378371

379372
let tmpDir = self.getTempFilePath()
380-
defer {
381-
try? FileManager.default.removeItem(at: tmpDir)
382-
}
383-
try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true)
373+
try await mkdir(atPath: tmpDir, parents: true)
374+
try await withTemporary(files: tmpDir) {
375+
await ctx.print("Extracting new swiftly...")
376+
try extractArchive(atPath: archive) { name in
377+
// Extract to the temporary directory
378+
tmpDir / String(name)
379+
}
384380

385-
await ctx.print("Extracting new swiftly...")
386-
try extractArchive(atPath: archive) { name in
387-
// Extract to the temporary directory
388-
tmpDir.appendingPathComponent(String(name))
381+
try self.runProgram((tmpDir / "swiftly").string, "init")
389382
}
390-
391-
try self.runProgram(tmpDir.appendingPathComponent("swiftly").path, "init")
392383
}
393384

394-
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool)
395-
throws
396-
{
397-
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(toolchain.name)
398-
try FileManager.default.removeItem(at: toolchainDir)
385+
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool) async throws {
386+
let toolchainDir = self.swiftlyToolchainsDir(ctx) / toolchain.name
387+
try await remove(atPath: toolchainDir)
399388
}
400389

401390
public func getExecutableName() -> String {
@@ -404,69 +393,65 @@ public struct Linux: Platform {
404393
return "swiftly-\(arch)-unknown-linux-gnu"
405394
}
406395

407-
public func getTempFilePath() -> URL {
408-
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())")
396+
public func getTempFilePath() -> FilePath {
397+
tmpDir / "swiftly-\(UUID())"
409398
}
410399

411400
public func verifyToolchainSignature(
412-
_ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: URL, verbose: Bool
401+
_ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: FilePath, verbose: Bool
413402
) async throws {
414403
if verbose {
415404
await ctx.print("Downloading toolchain signature...")
416405
}
417406

418407
let sigFile = self.getTempFilePath()
419-
let _ = FileManager.default.createFile(atPath: sigFile.path, contents: nil)
420-
defer {
421-
try? FileManager.default.removeItem(at: sigFile)
422-
}
423-
424-
try await ctx.httpClient.getSwiftToolchainFileSignature(toolchainFile).download(to: sigFile)
425-
426-
await ctx.print("Verifying toolchain signature...")
427-
do {
428-
if let mockedHomeDir = ctx.mockedHomeDir {
429-
try self.runProgram(
430-
"gpg", "--verify", sigFile.path, archive.path, quiet: false,
431-
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
432-
)
433-
} else {
434-
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose)
408+
try await create(file: sigFile, contents: nil)
409+
try await withTemporary(files: sigFile) {
410+
try await ctx.httpClient.getSwiftToolchainFileSignature(toolchainFile).download(to: sigFile)
411+
412+
await ctx.print("Verifying toolchain signature...")
413+
do {
414+
if let mockedHomeDir = ctx.mockedHomeDir {
415+
try self.runProgram(
416+
"gpg", "--verify", "\(sigFile)", "\(archive)", quiet: false,
417+
env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string]
418+
)
419+
} else {
420+
try self.runProgram("gpg", "--verify", "\(sigFile)", "\(archive)", quiet: !verbose)
421+
}
422+
} catch {
423+
throw SwiftlyError(message: "Signature verification failed: \(error).")
435424
}
436-
} catch {
437-
throw SwiftlyError(message: "Signature verification failed: \(error).")
438425
}
439426
}
440427

441428
public func verifySwiftlySignature(
442-
_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool
429+
_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: FilePath, verbose: Bool
443430
) async throws {
444431
if verbose {
445432
await ctx.print("Downloading swiftly signature...")
446433
}
447434

448435
let sigFile = self.getTempFilePath()
449-
let _ = FileManager.default.createFile(atPath: sigFile.path, contents: nil)
450-
defer {
451-
try? FileManager.default.removeItem(at: sigFile)
452-
}
453-
454-
try await ctx.httpClient.getSwiftlyReleaseSignature(
455-
url: archiveDownloadURL.appendingPathExtension("sig")
456-
).download(to: sigFile)
457-
458-
await ctx.print("Verifying swiftly signature...")
459-
do {
460-
if let mockedHomeDir = ctx.mockedHomeDir {
461-
try self.runProgram(
462-
"gpg", "--verify", sigFile.path, archive.path, quiet: false,
463-
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
464-
)
465-
} else {
466-
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose)
436+
try await create(file: sigFile, contents: nil)
437+
try await withTemporary(files: sigFile) {
438+
try await ctx.httpClient.getSwiftlyReleaseSignature(
439+
url: archiveDownloadURL.appendingPathExtension("sig")
440+
).download(to: sigFile)
441+
442+
await ctx.print("Verifying swiftly signature...")
443+
do {
444+
if let mockedHomeDir = ctx.mockedHomeDir {
445+
try self.runProgram(
446+
"gpg", "--verify", "\(sigFile)", "\(archive)", quiet: false,
447+
env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string]
448+
)
449+
} else {
450+
try self.runProgram("gpg", "--verify", "\(sigFile)", "\(archive)", quiet: !verbose)
451+
}
452+
} catch {
453+
throw SwiftlyError(message: "Signature verification failed: \(error).")
467454
}
468-
} catch {
469-
throw SwiftlyError(message: "Signature verification failed: \(error).")
470455
}
471456
}
472457

@@ -631,9 +616,9 @@ public struct Linux: Platform {
631616
return "/bin/bash"
632617
}
633618

634-
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL
619+
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> FilePath
635620
{
636-
self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.name)")
621+
self.swiftlyToolchainsDir(ctx) / "\(toolchain.name)"
637622
}
638623

639624
public static let currentPlatform: any Platform = Linux()

0 commit comments

Comments
 (0)