Skip to content

Commit 08118e4

Browse files
authored
Implement the use command on Linux platforms (#20)
1 parent d070b1e commit 08118e4

16 files changed

+644
-55
lines changed

Sources/LinuxPlatform/Linux.swift

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public struct Linux: Platform {
3636
throw Error(message: "\(tmpFile) doesn't exist")
3737
}
3838

39-
let toolchainsDir = swiftlyHomeDir.appendingPathComponent("toolchains")
39+
let toolchainsDir = SwiftlyCore.homeDir.appendingPathComponent("toolchains")
4040
if !toolchainsDir.fileExists() {
4141
try FileManager.default.createDirectory(at: toolchainsDir, withIntermediateDirectories: false)
4242
}
@@ -55,13 +55,36 @@ public struct Linux: Platform {
5555
// prepend ~/.swiftly/toolchains/<toolchain> to each file name
5656
return toolchainDir.appendingPathComponent(String(relativePath))
5757
}
58-
59-
// TODO: if config doesn't have an active toolchain, set it to that
6058
}
6159

6260
public func uninstall(version _: ToolchainVersion) throws {}
6361

64-
public func use(_: ToolchainVersion) throws {}
62+
public func use(_ toolchain: ToolchainVersion) throws {
63+
let toolchainBinURL = SwiftlyCore.toolchainsDir
64+
.appendingPathComponent(toolchain.name, isDirectory: true)
65+
.appendingPathComponent("usr", isDirectory: true)
66+
.appendingPathComponent("bin", isDirectory: true)
67+
68+
// Delete existing symlinks from previously in-use toolchain.
69+
for existingExecutable in try FileManager.default.contentsOfDirectory(atPath: SwiftlyCore.binDir.path) {
70+
guard existingExecutable != "swiftly" else {
71+
continue
72+
}
73+
try SwiftlyCore.binDir.appendingPathComponent(existingExecutable).deleteIfExists()
74+
}
75+
76+
for executable in try FileManager.default.contentsOfDirectory(atPath: toolchainBinURL.path) {
77+
let linkURL = SwiftlyCore.binDir.appendingPathComponent(executable)
78+
let executableURL = toolchainBinURL.appendingPathComponent(executable)
79+
80+
try linkURL.deleteIfExists()
81+
82+
try FileManager.default.createSymbolicLink(
83+
atPath: linkURL.path,
84+
withDestinationPath: executableURL.path
85+
)
86+
}
87+
}
6588

6689
public func listToolchains(selector _: ToolchainSelector?) -> [ToolchainVersion] {
6790
[]

Sources/Swiftly/Install.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,13 @@ struct Install: SwiftlyCommand {
144144

145145
try Swiftly.currentPlatform.install(from: tmpFile, version: version)
146146

147-
try Config.update { config in
148-
config.installedToolchains.insert(version)
147+
var config = try Config.load()
148+
config.installedToolchains.insert(version)
149+
try config.save()
150+
151+
// If this is the first installed toolchain, mark it as in-use.
152+
if config.inUse == nil {
153+
try await Use.execute(version)
149154
}
150155

151156
print("\(version) installed successfully!")

Sources/Swiftly/List.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ struct List: SwiftlyCommand {
3838
try ToolchainSelector(parsing: input)
3939
}
4040

41-
let toolchains = Swiftly.currentPlatform.listToolchains(selector: selector)
42-
let activeToolchain = try Swiftly.currentPlatform.currentToolchain()
41+
let config = try Config.load()
42+
43+
let toolchains = config.listInstalledToolchains(selector: selector)
44+
let activeToolchain = config.inUse
4345

4446
let printToolchain = { (toolchain: ToolchainVersion) in
4547
var message = "\(toolchain)"

Sources/Swiftly/ListAvailable.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,10 @@ struct ListAvailable: SwiftlyCommand {
4242
.map(ToolchainVersion.stable)
4343
.filter { selector?.matches(toolchain: $0) ?? true }
4444

45-
let installedToolchains = Set(Swiftly.currentPlatform.listToolchains(selector: selector))
46-
let activeToolchain = try Swiftly.currentPlatform.currentToolchain()
45+
let config = try Config.load()
46+
47+
let installedToolchains = Set(config.listInstalledToolchains(selector: selector))
48+
let activeToolchain = config.inUse
4749

4850
let printToolchain = { (toolchain: ToolchainVersion) in
4951
var message = "\(toolchain)"

Sources/Swiftly/Swiftly.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ public protocol SwiftlyCommand: AsyncParsableCommand {}
3737

3838
extension SwiftlyCommand {
3939
public mutating func validate() throws {
40-
do {
41-
_ = try Config.load()
42-
} catch {
43-
let msg = """
44-
Could not load swiftly's configuration file due to error: \"\(error)\".
45-
To use swiftly, modify the configuration file to fix the issue or perform a clean installation.
46-
"""
47-
throw Error(message: msg)
40+
for requiredDir in SwiftlyCore.requiredDirectories {
41+
guard requiredDir.fileExists() else {
42+
try FileManager.default.createDirectory(at: requiredDir, withIntermediateDirectories: true)
43+
continue
44+
}
4845
}
46+
47+
// Verify that the configuration exists and can be loaded
48+
_ = try Config.load()
4949
}
5050
}

Sources/Swiftly/Uninstall.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ struct Uninstall: SwiftlyCommand {
3535

3636
mutating func run() async throws {
3737
let selector = try ToolchainSelector(parsing: self.toolchain)
38-
let toolchains = Swiftly.currentPlatform.listToolchains(selector: selector)
38+
let config = try Config.load()
39+
let toolchains = config.listInstalledToolchains(selector: selector)
3940

4041
guard !toolchains.isEmpty else {
4142
print("no toolchains matched \"\(self.toolchain)\"")

Sources/Swiftly/Update.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,11 @@ struct Update: SwiftlyCommand {
6363

6464
private func oldToolchain() throws -> ToolchainVersion? {
6565
guard let input = self.toolchain else {
66-
return try Swiftly.currentPlatform.currentToolchain()
66+
return try Config.load().inUse
6767
}
6868

6969
let selector = try ToolchainSelector(parsing: input)
70-
let toolchains = Swiftly.currentPlatform.listToolchains(selector: selector)
70+
let toolchains = try Config.load().listInstalledToolchains(selector: selector)
7171

7272
// When multiple toolchains are matched, update the latest one.
7373
// This is for situations such as `swiftly update 5.5` when both

Sources/Swiftly/Use.swift

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ArgumentParser
22
import SwiftlyCore
33

4-
struct Use: SwiftlyCommand {
4+
internal struct Use: SwiftlyCommand {
55
public static var configuration = CommandConfiguration(
66
abstract: "Set the active toolchain."
77
)
@@ -40,21 +40,32 @@ struct Use: SwiftlyCommand {
4040

4141
internal mutating func run() async throws {
4242
let selector = try ToolchainSelector(parsing: self.toolchain)
43-
guard let toolchain = Swiftly.currentPlatform.listToolchains(selector: selector).max() else {
44-
print("no installed toolchains match \"\(self.toolchain)\"")
43+
let config = try Config.load()
44+
45+
guard let toolchain = config.listInstalledToolchains(selector: selector).max() else {
46+
print("No installed toolchains match \"\(self.toolchain)\"")
4547
return
4648
}
4749

48-
let old = try Swiftly.currentPlatform.currentToolchain()
50+
try await Self.execute(toolchain)
51+
}
52+
53+
internal static func execute(_ toolchain: ToolchainVersion) async throws {
54+
var config = try Config.load()
55+
let previousToolchain = config.inUse
4956

50-
try Swiftly.currentPlatform.use(toolchain)
51-
try Config.update { config in
52-
config.inUse = toolchain
57+
guard toolchain != previousToolchain else {
58+
print("\(toolchain) is already in use")
59+
return
5360
}
5461

55-
var message = "The current toolchain is now \(toolchain)"
56-
if let old {
57-
message += " (was \(old))"
62+
try Swiftly.currentPlatform.use(toolchain)
63+
config.inUse = toolchain
64+
try config.save()
65+
66+
var message = "Set the active toolchain to \(toolchain)"
67+
if let previousToolchain {
68+
message += " (was \(previousToolchain))"
5869
}
5970

6071
print(message)

Sources/SwiftlyCore/Config.swift

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import Foundation
22

3-
public var swiftlyHomeDir =
4-
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".swiftly", isDirectory: true)
5-
63
/// Struct modelling the config.json file used to track installed toolchains,
74
/// the current in-use tooolchain, and information about the platform.
85
///
@@ -18,10 +15,6 @@ public struct Config: Codable, Equatable {
1815
public var installedToolchains: Set<ToolchainVersion>
1916
public var platform: PlatformDefinition
2017

21-
// TODO: support other locations
22-
public static let fileName = "config.json"
23-
private static let url = swiftlyHomeDir.appendingPathComponent(Self.fileName)
24-
2518
internal init(inUse: ToolchainVersion?, installedToolchains: Set<ToolchainVersion>, platform: PlatformDefinition) {
2619
self.inUse = inUse
2720
self.installedToolchains = installedToolchains
@@ -36,14 +29,40 @@ public struct Config: Codable, Equatable {
3629

3730
/// Read the config file from disk.
3831
public static func load() throws -> Config {
39-
let data = try Data(contentsOf: Config.url)
40-
return try JSONDecoder().decode(Config.self, from: data)
32+
do {
33+
let data = try Data(contentsOf: SwiftlyCore.configFile)
34+
return try JSONDecoder().decode(Config.self, from: data)
35+
} catch {
36+
let msg = """
37+
Could not load swiftly's configuration file at \(SwiftlyCore.configFile.path) due to error: \"\(error)\".
38+
To use swiftly, modify the configuration file to fix the issue or perform a clean installation.
39+
"""
40+
throw Error(message: msg)
41+
}
4142
}
4243

4344
/// Write the contents of this `Config` struct to disk.
4445
public func save() throws {
4546
let outData = try Self.makeEncoder().encode(self)
46-
try outData.write(to: Config.url, options: .atomic)
47+
try outData.write(to: SwiftlyCore.configFile, options: .atomic)
48+
}
49+
50+
public func listInstalledToolchains(selector: ToolchainSelector?) -> [ToolchainVersion] {
51+
guard let selector else {
52+
return Array(self.installedToolchains)
53+
}
54+
55+
if case .latest = selector {
56+
var ts: [ToolchainVersion] = []
57+
if let t = self.installedToolchains.filter({ $0.isStableRelease() }).max() {
58+
ts.append(t)
59+
}
60+
return ts
61+
}
62+
63+
return self.installedToolchains.filter { toolchain in
64+
selector.matches(toolchain: toolchain)
65+
}
4766
}
4867

4968
/// Load the config, pass it to the provided closure, and then

Sources/SwiftlyCore/Platform.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@ public protocol Platform {
3434
/// Select the toolchain associated with the given version.
3535
func use(_ version: ToolchainVersion) throws
3636

37-
/// List the installed toolchains.
38-
func listToolchains(selector: ToolchainSelector?) -> [ToolchainVersion]
39-
4037
/// Get a list of snapshot builds for the platform. If a version is specified, only
4138
/// return snapshots associated with the version.
4239
/// This will likely have a default implementation.
@@ -46,9 +43,6 @@ public protocol Platform {
4643
/// This will likely have a default implementation.
4744
func selfUpdate() async throws
4845

49-
/// Get the toolchain that is currently "in use", if any.
50-
func currentToolchain() throws -> ToolchainVersion?
51-
5246
/// Get a path pointing to a unique, temporary file.
5347
/// This does not need to actually create the file.
5448
func getTempFilePath() -> URL

0 commit comments

Comments
 (0)