Skip to content

Commit 726ea90

Browse files
authoredMar 13, 2025··
Merge updates from release/1.0 to the main branch (#248)
* Update swiftly version to 1.0.0 and add upgrade routine for 0.4.0 (#237) * Make updates better and more resilient Put a check in-place to unset the global default toolchain if it is no longer installed Set the global default to the installed toolchain if it is not set Add full toolchain selection resolution to the update operation resolve update parameters Fix the use command and toolchain selection routine to consider a global default set to a toolchain that is not installed as no selection at all Add check for the physical presence of a toolchain to proxy so that it prevents circularity errors and provides an actionable message * Allow uninstalling of partially installed toolchains from config.json * Adjust error messages Add test cases Control verbosity of uninstall operation on macOS and other platforms * Make version changes for the main branch, and add upgrade path * Fix wrapping logic in init command to check for 1.0.x release * Remove hyphen from the swiftly version suffix * Fix version check in init
1 parent 315bd00 commit 726ea90

14 files changed

+108
-29
lines changed
 

‎Sources/LinuxPlatform/Linux.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ public struct Linux: Platform {
376376
try self.runProgram(tmpDir.appendingPathComponent("swiftly").path, "init")
377377
}
378378

379-
public func uninstall(_ toolchain: ToolchainVersion) throws {
379+
public func uninstall(_ toolchain: ToolchainVersion, verbose _: Bool) throws {
380380
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent(toolchain.name)
381381
try FileManager.default.removeItem(at: toolchainDir)
382382
}

‎Sources/MacOSPlatform/MacOS.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public struct MacOS: Platform {
120120
try self.runProgram(homeDir.appendingPathComponent("usr/local/bin/swiftly").path, "init")
121121
}
122122

123-
public func uninstall(_ toolchain: ToolchainVersion) throws {
123+
public func uninstall(_ toolchain: ToolchainVersion, verbose: Bool) throws {
124124
SwiftlyCore.print("Uninstalling package in user home directory...")
125125

126126
let toolchainDir = self.swiftlyToolchainsDir.appendingPathComponent("\(toolchain.identifier).xctoolchain", isDirectory: true)
@@ -138,7 +138,7 @@ public struct MacOS: Platform {
138138
try FileManager.default.removeItem(at: toolchainDir)
139139

140140
let homedir = ProcessInfo.processInfo.environment["HOME"]!
141-
try? runProgram("pkgutil", "--volume", homedir, "--forget", pkgInfo.CFBundleIdentifier)
141+
try? runProgram("pkgutil", "--volume", homedir, "--forget", pkgInfo.CFBundleIdentifier, quiet: !verbose)
142142
}
143143

144144
public func getExecutableName() -> String {

‎Sources/Swiftly/Init.swift

+8-2
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,14 @@ internal struct Init: SwiftlyCommand {
3737

3838
var config = try? Config.load()
3939

40-
if var config, !overwrite && config.version == SwiftlyVersion(major: 0, minor: 4, patch: 0, suffix: "dev") {
41-
// This is a simple upgrade from the 0.4.0-dev pre-release
40+
if var config, !overwrite &&
41+
(
42+
config.version == SwiftlyVersion(major: 0, minor: 4, patch: 0, suffix: "dev") ||
43+
config.version == SwiftlyVersion(major: 0, minor: 4, patch: 0) ||
44+
(config.version?.major == 1 && config.version?.minor == 0)
45+
)
46+
{
47+
// This is a simple upgrade from the 0.4.0 pre-releases, or 1.x
4248

4349
// Move our executable over to the correct place
4450
try Swiftly.currentPlatform.installSwiftlyBin()

‎Sources/Swiftly/Install.swift

+9-2
Original file line numberDiff line numberDiff line change
@@ -297,11 +297,18 @@ struct Install: SwiftlyCommand {
297297

298298
// If this is the first installed toolchain, mark it as in-use regardless of whether the
299299
// --use argument was provided.
300-
if useInstalledToolchain || config.inUse == nil {
301-
// TODO: consider adding the global default option to this commands flags
300+
if useInstalledToolchain {
302301
try await Use.execute(version, globalDefault: false, &config)
303302
}
304303

304+
// We always update the global default toolchain if there is none set. This could
305+
// be the only toolchain that is installed, which makes it the only choice.
306+
if config.inUse == nil {
307+
config.inUse = version
308+
try config.save()
309+
SwiftlyCore.print("The global default toolchain has been set to `\(version)`")
310+
}
311+
305312
SwiftlyCore.print("\(version) installed successfully!")
306313
return (postInstallScript, pathChanged)
307314
}

‎Sources/Swiftly/Proxy.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public enum Proxy {
5353
}
5454

5555
guard let toolchain = toolchain else {
56-
throw SwiftlyError(message: "No swift toolchain could be selected from either from a .swift-version file, or the default. You can try using `swiftly install <toolchain version>` to install one.")
56+
throw SwiftlyError(message: "No installed swift toolchain is selected from either from a .swift-version file, or the default. You can try using one that's already installed with `swiftly use <toolchain version>` or install a new toolchain to use with `swiftly install --use <toolchain version>`.")
5757
}
5858

5959
// Prevent circularities with a memento environment variable

‎Sources/Swiftly/Run.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ internal struct Run: SwiftlyCommand {
8686
}
8787

8888
guard let toolchain = toolchain else {
89-
throw SwiftlyError(message: "No swift toolchain could be selected from either from a .swift-version file, or the default. You can try using `swiftly install <toolchain version>` to install one.")
89+
throw SwiftlyError(message: "No installed swift toolchain is selected from either from a .swift-version file, or the default. You can try using one that's already installed with `swiftly use <toolchain version>` or install a new toolchain to use with `swiftly install --use <toolchain version>`.")
9090
}
9191

9292
do {

‎Sources/Swiftly/Uninstall.swift

+14-4
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,12 @@ struct Uninstall: SwiftlyCommand {
5656
}
5757
} else {
5858
let selector = try ToolchainSelector(parsing: self.toolchain)
59-
toolchains = startingConfig.listInstalledToolchains(selector: selector)
59+
var installedToolchains = startingConfig.listInstalledToolchains(selector: selector)
60+
// This is in the unusual case that the inUse toolchain is not listed in the installed toolchains
61+
if let inUse = startingConfig.inUse, selector.matches(toolchain: inUse) && !startingConfig.installedToolchains.contains(inUse) {
62+
installedToolchains.append(inUse)
63+
}
64+
toolchains = installedToolchains
6065
}
6166

6267
guard !toolchains.isEmpty else {
@@ -108,18 +113,23 @@ struct Uninstall: SwiftlyCommand {
108113
}
109114
}
110115

111-
try await Self.execute(toolchain, &config)
116+
try await Self.execute(toolchain, &config, verbose: self.root.verbose)
112117
}
113118

114119
SwiftlyCore.print()
115120
SwiftlyCore.print("\(toolchains.count) toolchain(s) successfully uninstalled")
116121
}
117122

118-
static func execute(_ toolchain: ToolchainVersion, _ config: inout Config) async throws {
123+
static func execute(_ toolchain: ToolchainVersion, _ config: inout Config, verbose: Bool) async throws {
119124
SwiftlyCore.print("Uninstalling \(toolchain)...", terminator: "")
120-
try Swiftly.currentPlatform.uninstall(toolchain)
121125
config.installedToolchains.remove(toolchain)
126+
// This is here to prevent the inUse from referencing a toolchain that is not installed
127+
if config.inUse == toolchain {
128+
config.inUse = nil
129+
}
122130
try config.save()
131+
132+
try Swiftly.currentPlatform.uninstall(toolchain, verbose: verbose)
123133
SwiftlyCore.print("done")
124134
}
125135
}

‎Sources/Swiftly/Update.swift

+5-5
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ struct Update: SwiftlyCommand {
8181
try validateSwiftly()
8282
var config = try Config.load()
8383

84-
guard let parameters = try self.resolveUpdateParameters(config) else {
84+
guard let parameters = try await self.resolveUpdateParameters(&config) else {
8585
if let toolchain = self.toolchain {
8686
SwiftlyCore.print("No installed toolchain matched \"\(toolchain)\"")
8787
} else {
@@ -101,7 +101,7 @@ struct Update: SwiftlyCommand {
101101
}
102102

103103
if !self.root.assumeYes {
104-
SwiftlyCore.print("Update \(parameters.oldToolchain) \(newToolchain)?")
104+
SwiftlyCore.print("Update \(parameters.oldToolchain) -> \(newToolchain)?")
105105
guard SwiftlyCore.promptForConfirmation(defaultBehavior: true) else {
106106
SwiftlyCore.print("Aborting")
107107
return
@@ -117,7 +117,7 @@ struct Update: SwiftlyCommand {
117117
assumeYes: self.root.assumeYes
118118
)
119119

120-
try await Uninstall.execute(parameters.oldToolchain, &config)
120+
try await Uninstall.execute(parameters.oldToolchain, &config, verbose: self.root.verbose)
121121
SwiftlyCore.print("Successfully updated \(parameters.oldToolchain)\(newToolchain)")
122122

123123
if let postInstallScript = postInstallScript {
@@ -152,7 +152,7 @@ struct Update: SwiftlyCommand {
152152
/// If the selector does not match an installed toolchain, this returns nil.
153153
/// If no selector is provided, the currently in-use toolchain will be used as the basis for the returned
154154
/// parameters.
155-
private func resolveUpdateParameters(_ config: Config) throws -> UpdateParameters? {
155+
private func resolveUpdateParameters(_ config: inout Config) async throws -> UpdateParameters? {
156156
let selector = try self.toolchain.map { try ToolchainSelector(parsing: $0) }
157157

158158
let oldToolchain: ToolchainVersion?
@@ -163,7 +163,7 @@ struct Update: SwiftlyCommand {
163163
// 5.5.1 and 5.5.2 are installed (5.5.2 will be updated).
164164
oldToolchain = toolchains.max()
165165
} else {
166-
oldToolchain = config.inUse
166+
(oldToolchain, _) = try await selectToolchain(config: &config)
167167
}
168168

169169
guard let oldToolchain else {

‎Sources/Swiftly/Use.swift

+10-3
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ internal struct Use: SwiftlyCommand {
133133
} else {
134134
config.inUse = toolchain
135135
try config.save()
136-
message = "The global default toolchain has set to `\(toolchain)`"
136+
message = "The global default toolchain has been set to `\(toolchain)`"
137137
}
138138

139139
if let selectedVersion = selectedVersion {
@@ -177,7 +177,8 @@ public enum ToolchainSelectionResult {
177177
/// Selection of a toolchain can be accomplished in a number of ways. There is the
178178
/// the configuration's global default 'inUse' setting. This is the fallback selector
179179
/// if there are no other selections. The returned tuple will contain the default toolchain
180-
/// version and the result will be .default.
180+
/// version and the result will be .globalDefault. This will always be the result if
181+
/// the globalDefault parameter is true.
181182
///
182183
/// A toolchain can also be selected from a `.swift-version` file in the current
183184
/// working directory, or an ancestor directory. If it successfully selects a toolchain
@@ -233,5 +234,11 @@ public func selectToolchain(config: inout Config, globalDefault: Bool = false) a
233234
}
234235
}
235236

236-
return (config.inUse, .globalDefault)
237+
// Check to ensure that the global default in use toolchain matches one of the installed toolchains, and return
238+
// no selected toolchain if it doesn't.
239+
guard let defaultInUse = config.inUse, config.installedToolchains.contains(defaultInUse) else {
240+
return (nil, .globalDefault)
241+
}
242+
243+
return (defaultInUse, .globalDefault)
237244
}

‎Sources/SwiftlyCore/Platform.swift

+4-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public protocol Platform {
7272

7373
/// Uninstalls a toolchain associated with the given version.
7474
/// If this version is in use, the next latest version will be used afterwards.
75-
func uninstall(_ version: ToolchainVersion) throws
75+
func uninstall(_ version: ToolchainVersion, verbose: Bool) throws
7676

7777
/// Get the name of the swiftly release binary.
7878
func getExecutableName() -> String
@@ -141,6 +141,9 @@ extension Platform {
141141
#if os(macOS) || os(Linux)
142142
internal func proxyEnv(_ toolchain: ToolchainVersion) throws -> [String: String] {
143143
let tcPath = self.findToolchainLocation(toolchain).appendingPathComponent("usr/bin")
144+
guard tcPath.fileExists() else {
145+
throw SwiftlyError(message: "Toolchain \(toolchain) could not be located. You can try `swiftly uninstall \(toolchain)` to uninstall it and then `swiftly install \(toolchain)` to install it again.")
146+
}
144147
var newEnv = ProcessInfo.processInfo.environment
145148

146149
// The toolchain goes to the beginning of the PATH

‎Sources/SwiftlyCore/SwiftlyCore.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Foundation
22

3-
public let version = SwiftlyVersion(major: 0, minor: 4, patch: 0)
3+
public let version = SwiftlyVersion(major: 1, minor: 1, patch: 0, suffix: "dev")
44

55
/// A separate home directory to use for testing purposes. This overrides swiftly's default
66
/// home directory location logic.

‎Tests/SwiftlyTests/PlatformTests.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -53,21 +53,21 @@ final class PlatformTests: SwiftlyTests {
5353
(mockedToolchainFile, version) = try await self.mockToolchainDownload(version: "5.6.3")
5454
try Swiftly.currentPlatform.install(from: mockedToolchainFile, version: version, verbose: true)
5555
// WHEN: one of the toolchains is uninstalled
56-
try Swiftly.currentPlatform.uninstall(version)
56+
try Swiftly.currentPlatform.uninstall(version, verbose: true)
5757
// THEN: there is only one remaining toolchain installed
5858
var toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil)
5959
XCTAssertEqual(1, toolchains.count)
6060

6161
// GIVEN; there is only one toolchain installed
6262
// WHEN: a non-existent toolchain is uninstalled
63-
try? Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.9.1"))
63+
try? Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.9.1"), verbose: true)
6464
// THEN: there is the one remaining toolchain that is still installed
6565
toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil)
6666
XCTAssertEqual(1, toolchains.count)
6767

6868
// GIVEN: there is only one toolchain installed
6969
// WHEN: the last toolchain is uninstalled
70-
try Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.8.0"))
70+
try Swiftly.currentPlatform.uninstall(ToolchainVersion(parsing: "5.8.0"), verbose: true)
7171
// THEN: there are no toolchains installed
7272
toolchains = try FileManager.default.contentsOfDirectory(at: Swiftly.currentPlatform.swiftlyToolchainsDir, includingPropertiesForKeys: nil)
7373
XCTAssertEqual(0, toolchains.count)

‎Tests/SwiftlyTests/UninstallTests.swift

+18
Original file line numberDiff line numberDiff line change
@@ -296,4 +296,22 @@ final class UninstallTests: SwiftlyTests {
296296
)
297297
}
298298
}
299+
300+
/// Tests that uninstalling a toolchain that is the global default, but is not in the list of installed toolchains.
301+
func testUninstallNotInstalled() async throws {
302+
let toolchains = Set([Self.oldStable, Self.newStable, Self.newMainSnapshot, Self.oldReleaseSnapshot])
303+
try await self.withMockedHome(homeName: Self.homeName, toolchains: toolchains, inUse: Self.newMainSnapshot) {
304+
var config = try await Config.load()
305+
config.inUse = Self.newMainSnapshot
306+
config.installedToolchains.remove(Self.newMainSnapshot)
307+
try await config.save()
308+
309+
var uninstall = try self.parseCommand(Uninstall.self, ["uninstall", "-y", Self.newMainSnapshot.name])
310+
_ = try await uninstall.run()
311+
try await self.validateInstalledToolchains(
312+
[Self.oldStable, Self.newStable, Self.oldReleaseSnapshot],
313+
description: "uninstall did not uninstall all toolchains"
314+
)
315+
}
316+
}
299317
}

‎Tests/SwiftlyTests/UpdateTests.swift

+31-3
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ final class UpdateTests: SwiftlyTests {
109109
}
110110
}
111111

112-
/// Verifies that updating the currently in-use toolchain can be updated, and that after update the new toolchain
113-
/// will be in-use instead.
114-
func testUpdateInUse() async throws {
112+
/// Verifies that updating the currently global default toolchain can be updated, and that after update the new toolchain
113+
/// will be the global default instead.
114+
func testUpdateGlobalDefault() async throws {
115115
try await self.withTestHome {
116116
try await self.withMockedToolchain {
117117
try await self.installMockedToolchain(selector: "6.0.0")
@@ -136,6 +136,34 @@ final class UpdateTests: SwiftlyTests {
136136
}
137137
}
138138

139+
/// Verifies that updating the currently in-use toolchain can be updated, and that after update the new toolchain
140+
/// will be in-use with the swift version file updated.
141+
func testUpdateInUse() async throws {
142+
try await self.withTestHome {
143+
try await self.withMockedToolchain {
144+
try await self.installMockedToolchain(selector: "6.0.0")
145+
146+
let versionFile = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(".swift-version")
147+
try "6.0.0".write(to: versionFile, atomically: true, encoding: .utf8)
148+
149+
var update = try self.parseCommand(Update.self, ["update", "-y", "--no-verify", "--post-install-file=\(Swiftly.currentPlatform.getTempFilePath().path)"])
150+
try await update.run()
151+
152+
let versionFileContents = try String(contentsOf: versionFile, encoding: .utf8)
153+
let inUse = try ToolchainVersion(parsing: versionFileContents)
154+
XCTAssertGreaterThan(inUse, .init(major: 6, minor: 0, patch: 0))
155+
156+
// Since the global default was set to 6.0.0, and that toolchain is no longer installed
157+
// the update should have unset it to prevent the config from going into a bad state.
158+
let config = try Config.load()
159+
XCTAssertTrue(config.inUse == nil)
160+
161+
// The new toolchain should be installed
162+
XCTAssertTrue(config.installedToolchains.contains(inUse))
163+
}
164+
}
165+
}
166+
139167
/// Verifies that snapshots, both from the main branch and from development branches, can be updated.
140168
func testUpdateSnapshot() async throws {
141169
let snapshotsAvailable = try await self.snapshotsAvailable()

0 commit comments

Comments
 (0)
Please sign in to comment.