Skip to content

Commit 4eddc22

Browse files
committed
DRAFT: Refactor global mutable state into a context migrate to swift testing
1 parent 351a278 commit 4eddc22

31 files changed

+1185
-1371
lines changed

Sources/LinuxPlatform/Linux.swift

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,21 @@ public struct Linux: Platform {
2929
}
3030
}
3131

32-
public var swiftlyBinDir: URL {
33-
SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) }
32+
public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL {
33+
ctx.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) }
3434
?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) }
3535
?? FileManager.default.homeDirectoryForCurrentUser
3636
.appendingPathComponent(".local/share/swiftly/bin", isDirectory: true)
3737
}
3838

39-
public var swiftlyToolchainsDir: URL {
40-
self.swiftlyHomeDir.appendingPathComponent("toolchains", isDirectory: true)
39+
public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL {
40+
self.swiftlyHomeDir(ctx).appendingPathComponent("toolchains", isDirectory: true)
4141
}
4242

4343
public var toolchainFileExtension: String {
4444
"tar.gz"
4545
}
4646

47-
public func isSystemDependencyPresent(_: SystemDependency) -> Bool {
48-
true
49-
}
50-
5147
private static let skipVerificationMessage: String = "To skip signature verification, specify the --no-verify flag."
5248

5349
public func verifySwiftlySystemPrerequisites() throws {
@@ -330,17 +326,17 @@ public struct Linux: Platform {
330326
}
331327
}
332328

333-
public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws {
329+
public func install(_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws {
334330
guard tmpFile.fileExists() else {
335331
throw SwiftlyError(message: "\(tmpFile) doesn't exist")
336332
}
337333

338-
if !self.swiftlyToolchainsDir.fileExists() {
339-
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false)
334+
if !self.swiftlyToolchainsDir(ctx).fileExists() {
335+
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false)
340336
}
341337

342-
SwiftlyCore.print("Extracting toolchain...")
343-
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent(version.name)
338+
SwiftlyCore.print(ctx, "Extracting toolchain...")
339+
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(version.name)
344340

345341
if toolchainDir.fileExists() {
346342
try FileManager.default.removeItem(at: toolchainDir)
@@ -354,23 +350,23 @@ public struct Linux: Platform {
354350
let destination = toolchainDir.appendingPathComponent(String(relativePath))
355351

356352
if verbose {
357-
SwiftlyCore.print("\(destination.path)")
353+
SwiftlyCore.print(ctx, "\(destination.path)")
358354
}
359355

360356
// prepend /path/to/swiftlyHomeDir/toolchains/<toolchain> to each file name
361357
return destination
362358
}
363359
}
364360

365-
public func extractSwiftlyAndInstall(from archive: URL) throws {
361+
public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws {
366362
guard archive.fileExists() else {
367363
throw SwiftlyError(message: "\(archive) doesn't exist")
368364
}
369365

370366
let tmpDir = self.getTempFilePath()
371367
try FileManager.default.createDirectory(atPath: tmpDir.path, withIntermediateDirectories: true)
372368

373-
SwiftlyCore.print("Extracting new swiftly...")
369+
SwiftlyCore.print(ctx, "Extracting new swiftly...")
374370
try extractArchive(atPath: archive) { name in
375371
// Extract to the temporary directory
376372
tmpDir.appendingPathComponent(String(name))
@@ -379,8 +375,8 @@ public struct Linux: Platform {
379375
try self.runProgram(tmpDir.appendingPathComponent("swiftly").path, "init")
380376
}
381377

382-
public func uninstall(_ toolchain: ToolchainVersion, verbose _: Bool) throws {
383-
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent(toolchain.name)
378+
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool) throws {
379+
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(toolchain.name)
384380
try FileManager.default.removeItem(at: toolchainDir)
385381
}
386382

@@ -394,9 +390,9 @@ public struct Linux: Platform {
394390
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())")
395391
}
396392

397-
public func verifySignature(httpClient: SwiftlyHTTPClient, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws {
393+
public func verifySignature(_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws {
398394
if verbose {
399-
SwiftlyCore.print("Downloading toolchain signature...")
395+
SwiftlyCore.print(ctx, "Downloading toolchain signature...")
400396
}
401397

402398
let sigFile = self.getTempFilePath()
@@ -405,20 +401,20 @@ public struct Linux: Platform {
405401
try? FileManager.default.removeItem(at: sigFile)
406402
}
407403

408-
try await httpClient.downloadFile(
404+
try await ctx.httpClient.downloadFile(
409405
url: archiveDownloadURL.appendingPathExtension("sig"),
410406
to: sigFile
411407
)
412408

413-
SwiftlyCore.print("Verifying toolchain signature...")
409+
SwiftlyCore.print(ctx, "Verifying toolchain signature...")
414410
do {
415411
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose)
416412
} catch {
417413
throw SwiftlyError(message: "Signature verification failed: \(error).")
418414
}
419415
}
420416

421-
private func manualSelectPlatform(_ platformPretty: String?) async -> PlatformDefinition {
417+
private func manualSelectPlatform(_ ctx: SwiftlyCoreContext, _ platformPretty: String?) async -> PlatformDefinition {
422418
if let platformPretty {
423419
print("\(platformPretty) is not an officially supported platform, but the toolchains for another platform may still work on it.")
424420
} else {
@@ -434,7 +430,7 @@ public struct Linux: Platform {
434430
\(selections)
435431
""")
436432

437-
let choice = SwiftlyCore.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0"
433+
let choice = SwiftlyCore.readLine(ctx, prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0"
438434

439435
guard let choiceNum = Int(choice) else {
440436
fatalError("Installation canceled")
@@ -447,7 +443,7 @@ public struct Linux: Platform {
447443
return self.linuxPlatforms[choiceNum - 1]
448444
}
449445

450-
public func detectPlatform(disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition {
446+
public func detectPlatform(_ ctx: SwiftlyCoreContext, disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition {
451447
// We've been given a hint to use
452448
if let platform {
453449
guard let pd = linuxPlatforms.first(where: { $0.nameFull == platform }) else {
@@ -475,7 +471,7 @@ public struct Linux: Platform {
475471
} else {
476472
print(message)
477473
}
478-
return await self.manualSelectPlatform(platformPretty)
474+
return await self.manualSelectPlatform(ctx, platformPretty)
479475
}
480476

481477
let releaseInfo = try String(contentsOfFile: releaseFile, encoding: .utf8)
@@ -502,7 +498,7 @@ public struct Linux: Platform {
502498
} else {
503499
print(message)
504500
}
505-
return await self.manualSelectPlatform(platformPretty)
501+
return await self.manualSelectPlatform(ctx, platformPretty)
506502
}
507503

508504
if (id + (idlike ?? "")).contains("amzn") {
@@ -513,7 +509,7 @@ public struct Linux: Platform {
513509
} else {
514510
print(message)
515511
}
516-
return await self.manualSelectPlatform(platformPretty)
512+
return await self.manualSelectPlatform(ctx, platformPretty)
517513
}
518514

519515
return .amazonlinux2
@@ -525,7 +521,7 @@ public struct Linux: Platform {
525521
} else {
526522
print(message)
527523
}
528-
return await self.manualSelectPlatform(platformPretty)
524+
return await self.manualSelectPlatform(ctx, platformPretty)
529525
}
530526

531527
return .rhel9
@@ -539,7 +535,7 @@ public struct Linux: Platform {
539535
} else {
540536
print(message)
541537
}
542-
return await self.manualSelectPlatform(platformPretty)
538+
return await self.manualSelectPlatform(ctx, platformPretty)
543539
}
544540

545541
public func getShell() async throws -> String {
@@ -559,8 +555,8 @@ public struct Linux: Platform {
559555
return "/bin/bash"
560556
}
561557

562-
public func findToolchainLocation(_ toolchain: ToolchainVersion) -> URL {
563-
self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.name)")
558+
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL {
559+
self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.name)")
564560
}
565561

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

Sources/MacOSPlatform/MacOS.swift

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,15 @@ public struct MacOS: Platform {
1818
.appendingPathComponent(".swiftly", isDirectory: true)
1919
}
2020

21-
public var swiftlyBinDir: URL {
22-
SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) }
21+
public func swiftlyBinDir(_ ctx: SwiftlyCoreContext) -> URL {
22+
ctx.mockedHomeDir.map { $0.appendingPathComponent("bin", isDirectory: true) }
2323
?? ProcessInfo.processInfo.environment["SWIFTLY_BIN_DIR"].map { URL(fileURLWithPath: $0) }
2424
?? FileManager.default.homeDirectoryForCurrentUser
2525
.appendingPathComponent(".swiftly/bin", isDirectory: true)
2626
}
2727

28-
public var swiftlyToolchainsDir: URL {
29-
SwiftlyCore.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) }
28+
public func swiftlyToolchainsDir(_ ctx: SwiftlyCoreContext) -> URL {
29+
ctx.mockedHomeDir.map { $0.appendingPathComponent("Toolchains", isDirectory: true) }
3030
// The toolchains are always installed here by the installer. We bypass the installer in the case of test mocks
3131
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Developer/Toolchains", isDirectory: true)
3232
}
@@ -35,11 +35,6 @@ public struct MacOS: Platform {
3535
"pkg"
3636
}
3737

38-
public func isSystemDependencyPresent(_: SystemDependency) -> Bool {
39-
// All system dependencies on macOS should be present
40-
true
41-
}
42-
4338
public func verifySwiftlySystemPrerequisites() throws {
4439
// All system prerequisites are there for swiftly on macOS
4540
}
@@ -49,24 +44,24 @@ public struct MacOS: Platform {
4944
nil
5045
}
5146

52-
public func install(from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws {
47+
public func install(_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws {
5348
guard tmpFile.fileExists() else {
5449
throw SwiftlyError(message: "\(tmpFile) doesn't exist")
5550
}
5651

57-
if !self.swiftlyToolchainsDir.fileExists() {
58-
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir, withIntermediateDirectories: false)
52+
if !self.swiftlyToolchainsDir(ctx).fileExists() {
53+
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false)
5954
}
6055

61-
if SwiftlyCore.mockedHomeDir == nil {
62-
SwiftlyCore.print("Installing package in user home directory...")
56+
if ctx.mockedHomeDir == nil {
57+
SwiftlyCore.print(ctx, "Installing package in user home directory...")
6358
try runProgram("installer", "-verbose", "-pkg", tmpFile.path, "-target", "CurrentUserHomeDirectory", quiet: !verbose)
6459
} else {
6560
// In the case of a mock for testing purposes we won't use the installer, perferring a manual process because
6661
// the installer will not install to an arbitrary path, only a volume or user home directory.
67-
SwiftlyCore.print("Expanding pkg...")
62+
SwiftlyCore.print(ctx, "Expanding pkg...")
6863
let tmpDir = self.getTempFilePath()
69-
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true)
64+
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(version.identifier).xctoolchain", isDirectory: true)
7065
if !toolchainDir.fileExists() {
7166
try FileManager.default.createDirectory(at: toolchainDir, withIntermediateDirectories: false)
7267
}
@@ -78,26 +73,26 @@ public struct MacOS: Platform {
7873
payload = tmpDir.appendingPathComponent("\(version.identifier)-osx-package.pkg/Payload")
7974
}
8075

81-
SwiftlyCore.print("Untarring pkg Payload...")
76+
SwiftlyCore.print(ctx, "Untarring pkg Payload...")
8277
try runProgram("tar", "-C", toolchainDir.path, "-xvf", payload.path, quiet: !verbose)
8378
}
8479
}
8580

86-
public func extractSwiftlyAndInstall(from archive: URL) throws {
81+
public func extractSwiftlyAndInstall(_ ctx: SwiftlyCoreContext, from archive: URL) throws {
8782
guard archive.fileExists() else {
8883
throw SwiftlyError(message: "\(archive) doesn't exist")
8984
}
9085

9186
let homeDir: URL
9287

93-
if SwiftlyCore.mockedHomeDir == nil {
88+
if ctx.mockedHomeDir == nil {
9489
homeDir = FileManager.default.homeDirectoryForCurrentUser
9590

96-
SwiftlyCore.print("Extracting the swiftly package...")
91+
SwiftlyCore.print(ctx, "Extracting the swiftly package...")
9792
try runProgram("installer", "-pkg", archive.path, "-target", "CurrentUserHomeDirectory")
9893
try? runProgram("pkgutil", "--volume", homeDir.path, "--forget", "org.swift.swiftly")
9994
} else {
100-
homeDir = SwiftlyCore.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser
95+
homeDir = ctx.mockedHomeDir ?? FileManager.default.homeDirectoryForCurrentUser
10196

10297
let installDir = homeDir.appendingPathComponent(".swiftly")
10398
try FileManager.default.createDirectory(atPath: installDir.path, withIntermediateDirectories: true)
@@ -114,17 +109,17 @@ public struct MacOS: Platform {
114109
throw SwiftlyError(message: "Payload file could not be found at \(tmpDir).")
115110
}
116111

117-
SwiftlyCore.print("Extracting the swiftly package into \(installDir.path)...")
112+
SwiftlyCore.print(ctx, "Extracting the swiftly package into \(installDir.path)...")
118113
try runProgram("tar", "-C", installDir.path, "-xvf", payload.path, quiet: false)
119114
}
120115

121116
try self.runProgram(homeDir.appendingPathComponent(".swiftly/bin/swiftly").path, "init")
122117
}
123118

124-
public func uninstall(_ toolchain: ToolchainVersion, verbose: Bool) throws {
125-
SwiftlyCore.print("Uninstalling package in user home directory...")
119+
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose: Bool) throws {
120+
SwiftlyCore.print(ctx, "Uninstalling package in user home directory...")
126121

127-
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true)
122+
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true)
128123

129124
let decoder = PropertyListDecoder()
130125
let infoPlist = toolchainDir.appendingPathComponent("Info.plist")
@@ -150,12 +145,12 @@ public struct MacOS: Platform {
150145
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID()).pkg")
151146
}
152147

153-
public func verifySignature(httpClient _: SwiftlyHTTPClient, archiveDownloadURL _: URL, archive _: URL, verbose _: Bool) async throws {
148+
public func verifySignature(_: SwiftlyCoreContext, archiveDownloadURL _: URL, archive _: URL, verbose _: Bool) async throws {
154149
// No signature verification is required on macOS since the pkg files have their own signing
155150
// mechanism and the swift.org downloadables are trusted by stock macOS installations.
156151
}
157152

158-
public func detectPlatform(disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition {
153+
public func detectPlatform(_: SwiftlyCoreContext, disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition {
159154
// No special detection required on macOS platform
160155
.macOS
161156
}
@@ -175,8 +170,8 @@ public struct MacOS: Platform {
175170
return "/bin/zsh"
176171
}
177172

178-
public func findToolchainLocation(_ toolchain: ToolchainVersion) -> URL {
179-
self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain")
173+
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL {
174+
self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.identifier).xctoolchain")
180175
}
181176

182177
public static let currentPlatform: any Platform = MacOS()

Sources/Swiftly/Config.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ public struct Config: Codable, Equatable {
2424
}
2525

2626
/// Read the config file from disk.
27-
public static func load() throws -> Config {
27+
public static func load(_ ctx: SwiftlyCoreContext) throws -> Config {
2828
do {
29-
let data = try Data(contentsOf: Swiftly.currentPlatform.swiftlyConfigFile)
29+
let data = try Data(contentsOf: Swiftly.currentPlatform.swiftlyConfigFile(ctx))
3030
var config = try JSONDecoder().decode(Config.self, from: data)
3131
if config.version == nil {
3232
// Assume that the version of swiftly is 0.3.0 because that is the last
@@ -36,7 +36,7 @@ public struct Config: Codable, Equatable {
3636
return config
3737
} catch {
3838
let msg = """
39-
Could not load swiftly's configuration file at \(Swiftly.currentPlatform.swiftlyConfigFile.path).
39+
Could not load swiftly's configuration file at \(Swiftly.currentPlatform.swiftlyConfigFile(ctx).path).
4040
4141
To begin using swiftly you can install it: '\(CommandLine.arguments[0]) init'.
4242
"""
@@ -45,9 +45,9 @@ public struct Config: Codable, Equatable {
4545
}
4646

4747
/// Write the contents of this `Config` struct to disk.
48-
public func save() throws {
48+
public func save(_ ctx: SwiftlyCoreContext) throws {
4949
let outData = try Self.makeEncoder().encode(self)
50-
try outData.write(to: Swiftly.currentPlatform.swiftlyConfigFile, options: .atomic)
50+
try outData.write(to: Swiftly.currentPlatform.swiftlyConfigFile(ctx), options: .atomic)
5151
}
5252

5353
public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] {
@@ -70,11 +70,11 @@ public struct Config: Codable, Equatable {
7070

7171
/// Load the config, pass it to the provided closure, and then
7272
/// save the modified config to disk.
73-
public static func update(f: (inout Config) throws -> Void) throws {
74-
var config = try Config.load()
73+
public static func update(_ ctx: SwiftlyCoreContext, f: (inout Config) throws -> Void) throws {
74+
var config = try Config.load(ctx)
7575
try f(&config)
7676
// only save the updates if the prior closure invocation succeeded
77-
try config.save()
77+
try config.save(ctx)
7878
}
7979
}
8080

0 commit comments

Comments
 (0)