@@ -6,11 +6,24 @@ struct Uninstall: SwiftlyCommand {
6
6
abstract: " Remove an installed toolchain. "
7
7
)
8
8
9
+ private enum UninstallConstants {
10
+ static let allSelector = " all "
11
+ }
12
+
13
+ private struct UninstallCancelledError : Error { }
14
+
15
+ private struct ToolchainSelectionResult {
16
+ let validToolchains : Set < ToolchainVersion >
17
+ let selectorToToolchains : [ String : [ ToolchainVersion ] ]
18
+ let invalidSelectors : [ String ]
19
+ let noMatchSelectors : [ String ]
20
+ }
21
+
9
22
@Argument ( help: ArgumentHelp (
10
23
" The toolchain(s) to uninstall. " ,
11
24
discussion: """
12
25
13
- The toolchain selector provided determines which toolchains to uninstall. Specific \
26
+ The list of toolchain selectors determines which toolchains to uninstall. Specific \
14
27
toolchains can be uninstalled by using their full names as the selector, for example \
15
28
a full stable release version with patch (a.b.c):
16
29
@@ -20,6 +33,10 @@ struct Uninstall: SwiftlyCommand {
20
33
21
34
$ swiftly uninstall 5.7-snapshot-2022-06-20
22
35
36
+ Multiple toolchain selectors can uninstall multiple toolchains at once:
37
+
38
+ $ swiftly uninstall 5.2.1 6.0.1
39
+
23
40
Less specific selectors can be used to uninstall multiple toolchains at once. For instance, \
24
41
the patch version can be omitted to uninstall all toolchains associated with a given minor version release:
25
42
@@ -39,7 +56,7 @@ struct Uninstall: SwiftlyCommand {
39
56
$ swiftly uninstall all
40
57
"""
41
58
) )
42
- var toolchain : String
59
+ var toolchains : [ String ]
43
60
44
61
@OptionGroup var root : GlobalOptions
45
62
@@ -54,87 +71,254 @@ struct Uninstall: SwiftlyCommand {
54
71
}
55
72
56
73
let startingConfig = try await Config . load ( ctx)
74
+ let selectionResult = try await parseAndValidateToolchainSelectors ( startingConfig)
75
+ let confirmedToolchains = try await handleErrorsAndGetConfirmation ( ctx, selectionResult)
57
76
58
- var toolchains : [ ToolchainVersion ]
59
- if self . toolchain == " all " {
60
- // Sort the uninstalled toolchains such that the in-use toolchain will be uninstalled last.
61
- // This avoids printing any unnecessary output from using new toolchains while the uninstall is in progress.
62
- toolchains = startingConfig. listInstalledToolchains ( selector: nil ) . sorted { a, b in
63
- a != startingConfig. inUse && ( b == startingConfig. inUse || a < b)
64
- }
65
- } else {
66
- let selector = try ToolchainSelector ( parsing: self . toolchain)
67
- var installedToolchains = startingConfig. listInstalledToolchains ( selector: selector)
68
- // This is in the unusual case that the inUse toolchain is not listed in the installed toolchains
69
- if let inUse = startingConfig. inUse, selector. matches ( toolchain: inUse) && !startingConfig. installedToolchains. contains ( inUse) {
70
- installedToolchains. append ( inUse)
77
+ try await executeUninstalls ( ctx, confirmedToolchains, startingConfig)
78
+ }
79
+
80
+ private func parseAndValidateToolchainSelectors( _ config: Config ) async throws -> ToolchainSelectionResult {
81
+ var allToolchains : Set < ToolchainVersion > = Set ( )
82
+ var selectorToToolchains : [ String : [ ToolchainVersion ] ] = [ : ]
83
+ var invalidSelectors : [ String ] = [ ]
84
+ var noMatchSelectors : [ String ] = [ ]
85
+
86
+ for toolchainSelector in self . toolchains {
87
+ if toolchainSelector == UninstallConstants . allSelector {
88
+ let allInstalledToolchains = self . processAllSelector ( config)
89
+ allToolchains. formUnion ( allInstalledToolchains)
90
+ selectorToToolchains [ toolchainSelector] = allInstalledToolchains
91
+ } else {
92
+ do {
93
+ let installedToolchains = try processIndividualSelector ( toolchainSelector, config)
94
+
95
+ if installedToolchains. isEmpty {
96
+ noMatchSelectors. append ( toolchainSelector)
97
+ } else {
98
+ allToolchains. formUnion ( installedToolchains)
99
+ selectorToToolchains [ toolchainSelector] = installedToolchains
100
+ }
101
+ } catch {
102
+ invalidSelectors. append ( toolchainSelector)
103
+ }
71
104
}
72
- toolchains = installedToolchains
73
105
}
74
106
75
- // Filter out the xcode toolchain here since it is not uninstallable
76
- toolchains. removeAll ( where: { $0 == . xcodeVersion } )
107
+ return ToolchainSelectionResult (
108
+ validToolchains: allToolchains,
109
+ selectorToToolchains: selectorToToolchains,
110
+ invalidSelectors: invalidSelectors,
111
+ noMatchSelectors: noMatchSelectors
112
+ )
113
+ }
114
+
115
+ private func processAllSelector( _ config: Config ) -> [ ToolchainVersion ] {
116
+ config. listInstalledToolchains ( selector: nil ) . sorted { a, b in
117
+ a != config. inUse && ( b == config. inUse || a < b)
118
+ }
119
+ }
120
+
121
+ private func processIndividualSelector( _ selector: String , _ config: Config ) throws -> [ ToolchainVersion ] {
122
+ let toolchainSelector = try ToolchainSelector ( parsing: selector)
123
+ var installedToolchains = config. listInstalledToolchains ( selector: toolchainSelector)
124
+
125
+ // This handles the unusual case that the inUse toolchain is not listed in the installed toolchains
126
+ if let inUse = config. inUse, toolchainSelector. matches ( toolchain: inUse) && !config. installedToolchains. contains ( inUse) {
127
+ installedToolchains. append ( inUse)
128
+ }
129
+
130
+ return installedToolchains
131
+ }
132
+
133
+ private func handleErrorsAndGetConfirmation(
134
+ _ ctx: SwiftlyCoreContext ,
135
+ _ selectionResult: ToolchainSelectionResult
136
+ ) async throws -> [ ToolchainVersion ] {
137
+ if self . hasErrors ( selectionResult) {
138
+ try await self . handleSelectionErrors ( ctx, selectionResult)
139
+ }
140
+
141
+ let toolchains = self . prepareToolchainsForUninstall ( selectionResult)
77
142
78
143
guard !toolchains. isEmpty else {
79
- await ctx. message ( " No toolchains can be uninstalled that match \" \( self . toolchain) \" " )
80
- return
144
+ if self . toolchains. count == 1 {
145
+ await ctx. message ( " No toolchains can be uninstalled that match \" \( self . toolchains [ 0 ] ) \" " )
146
+ } else {
147
+ await ctx. message ( " No toolchains can be uninstalled that match the provided selectors " )
148
+ }
149
+ throw UninstallCancelledError ( )
81
150
}
82
151
83
152
if !self . root. assumeYes {
84
- await ctx. message ( " The following toolchains will be uninstalled: " )
153
+ try await self . confirmUninstallation ( ctx, toolchains, selectionResult. selectorToToolchains)
154
+ }
85
155
86
- for toolchain in toolchains {
87
- await ctx. message ( " \( toolchain) " )
88
- }
156
+ return toolchains
157
+ }
158
+
159
+ private func hasErrors( _ result: ToolchainSelectionResult ) -> Bool {
160
+ !result. invalidSelectors. isEmpty || !result. noMatchSelectors. isEmpty
161
+ }
162
+
163
+ private func handleSelectionErrors( _ ctx: SwiftlyCoreContext , _ result: ToolchainSelectionResult ) async throws {
164
+ var errorMessages : [ String ] = [ ]
89
165
90
- guard await ctx. promptForConfirmation ( defaultBehavior: true ) else {
166
+ if !result. invalidSelectors. isEmpty {
167
+ errorMessages. append ( " Invalid toolchain selectors: \( result. invalidSelectors. joined ( separator: " , " ) ) " )
168
+ }
169
+
170
+ if !result. noMatchSelectors. isEmpty {
171
+ errorMessages. append ( " No toolchains match these selectors: \( result. noMatchSelectors. joined ( separator: " , " ) ) " )
172
+ }
173
+
174
+ for message in errorMessages {
175
+ await ctx. message ( message)
176
+ }
177
+
178
+ // If we have some valid selections, ask user if they want to proceed
179
+ if !result. validToolchains. isEmpty {
180
+ await ctx. message ( " \n Found \( result. validToolchains. count) toolchain(s) from valid selectors. Continue with uninstalling these? " )
181
+ guard await ctx. promptForConfirmation ( defaultBehavior: false ) else {
91
182
await ctx. message ( " Aborting uninstall " )
92
- return
183
+ throw UninstallCancelledError ( )
93
184
}
185
+ } else {
186
+ // No valid toolchains found at all
187
+ await ctx. message ( " No valid toolchains found to uninstall. " )
188
+ throw UninstallCancelledError ( )
189
+ }
190
+ }
191
+
192
+ private func prepareToolchainsForUninstall( _ selectionResult: ToolchainSelectionResult ) -> [ ToolchainVersion ] {
193
+ // Convert Set back to Array - sorting will be done in execution phase with proper config access
194
+ var toolchains = Array ( selectionResult. validToolchains)
195
+
196
+ // Filter out the xcode toolchain here since it is not uninstallable
197
+ toolchains. removeAll ( where: { $0 == . xcodeVersion } )
198
+
199
+ return toolchains
200
+ }
201
+
202
+ private func confirmUninstallation(
203
+ _ ctx: SwiftlyCoreContext ,
204
+ _ toolchains: [ ToolchainVersion ] ,
205
+ _ _: [ String : [ ToolchainVersion ] ]
206
+ ) async throws {
207
+ await self . displayToolchainConfirmation ( ctx, toolchains)
208
+
209
+ guard await ctx. promptForConfirmation ( defaultBehavior: true ) else {
210
+ await ctx. message ( " Aborting uninstall " )
211
+ throw UninstallCancelledError ( )
212
+ }
213
+ }
214
+
215
+ private func displayToolchainConfirmation( _ ctx: SwiftlyCoreContext , _ toolchains: [ ToolchainVersion ] ) async {
216
+ await ctx. message ( " The following toolchains will be uninstalled: " )
217
+ for toolchain in toolchains. sorted ( ) {
218
+ await ctx. message ( " \( toolchain) " )
94
219
}
220
+ }
95
221
222
+ private func executeUninstalls(
223
+ _ ctx: SwiftlyCoreContext ,
224
+ _ toolchains: [ ToolchainVersion ] ,
225
+ _ startingConfig: Config
226
+ ) async throws {
96
227
await ctx. message ( )
97
228
98
- for toolchain in toolchains {
229
+ // Apply proper sorting with access to config
230
+ let sortedToolchains = self . applySortingStrategy ( toolchains, config: startingConfig)
231
+
232
+ for (index, toolchain) in sortedToolchains. enumerated ( ) {
233
+ await self . displayProgress ( ctx, index: index, total: sortedToolchains. count, toolchain: toolchain)
234
+
99
235
var config = try await Config . load ( ctx)
100
236
101
- // If the in-use toolchain was one of the uninstalled toolchains, use a new toolchain.
102
237
if toolchain == config. inUse {
103
- let selector : ToolchainSelector
104
- switch toolchain {
105
- case let . stable( sr) :
106
- // If a.b.c was previously in use, switch to the latest a.b toolchain.
107
- selector = . stable( major: sr. major, minor: sr. minor, patch: nil )
108
- case let . snapshot( s) :
109
- // If a snapshot was previously in use, switch to the latest snapshot associated with that branch.
110
- selector = . snapshot( branch: s. branch, date: nil )
111
- case . xcode:
112
- // Xcode will not be in the list of installed toolchains, so this is only here for completeness
113
- selector = . xcode
114
- }
115
-
116
- if let toUse = config. listInstalledToolchains ( selector: selector)
117
- . filter ( { !toolchains. contains ( $0) } )
118
- . max ( )
119
- ?? config. listInstalledToolchains ( selector: . latest) . filter ( { !toolchains. contains ( $0) } ) . max ( )
120
- ?? config. installedToolchains. filter ( { !toolchains. contains ( $0) } ) . max ( )
121
- {
122
- let pathChanged = try await Use . execute ( ctx, toUse, globalDefault: true , verbose: self . root. verbose, & config)
123
- if pathChanged {
124
- try await Self . handlePathChange ( ctx)
125
- }
126
- } else {
127
- // If there are no more toolchains installed, just unuse the currently active toolchain.
128
- config. inUse = nil
129
- try config. save ( ctx)
130
- }
238
+ try await self . handleInUseToolchainReplacement ( ctx, toolchain, sortedToolchains, & config)
131
239
}
132
240
133
241
try await Self . execute ( ctx, toolchain, & config, verbose: self . root. verbose)
134
242
}
135
243
244
+ await self . displayCompletionMessage ( ctx, sortedToolchains. count)
245
+ }
246
+
247
+ private func applySortingStrategy( _ toolchains: [ ToolchainVersion ] , config: Config ) -> [ ToolchainVersion ] {
248
+ toolchains. sorted { a, b in
249
+ a != config. inUse && ( b == config. inUse || a < b)
250
+ }
251
+ }
252
+
253
+ private func handleInUseToolchainReplacement(
254
+ _ ctx: SwiftlyCoreContext ,
255
+ _ toolchain: ToolchainVersion ,
256
+ _ allUninstallTargets: [ ToolchainVersion ] ,
257
+ _ config: inout Config
258
+ ) async throws {
259
+ let replacementSelector = self . createReplacementSelector ( for: toolchain)
260
+
261
+ if let replacement = self . findSuitableReplacement ( config, replacementSelector, excluding: allUninstallTargets) {
262
+ let pathChanged = try await Use . execute ( ctx, replacement, globalDefault: true , verbose: self . root. verbose, & config)
263
+ if pathChanged {
264
+ try await Self . handlePathChange ( ctx)
265
+ }
266
+ } else {
267
+ config. inUse = nil
268
+ try config. save ( ctx)
269
+ }
270
+ }
271
+
272
+ private func createReplacementSelector( for toolchain: ToolchainVersion ) -> ToolchainSelector {
273
+ switch toolchain {
274
+ case let . stable( sr) :
275
+ // If a.b.c was previously in use, switch to the latest a.b toolchain.
276
+ return . stable( major: sr. major, minor: sr. minor, patch: nil )
277
+ case let . snapshot( s) :
278
+ // If a snapshot was previously in use, switch to the latest snapshot associated with that branch.
279
+ return . snapshot( branch: s. branch, date: nil )
280
+ case . xcode:
281
+ // Xcode will not be in the list of installed toolchains, so this is only here for completeness
282
+ return . xcode
283
+ }
284
+ }
285
+
286
+ private func findSuitableReplacement(
287
+ _ config: Config ,
288
+ _ selector: ToolchainSelector ,
289
+ excluding: [ ToolchainVersion ]
290
+ ) -> ToolchainVersion ? {
291
+ // Try the specific selector first
292
+ if let replacement = config. listInstalledToolchains ( selector: selector)
293
+ . filter ( { !excluding. contains ( $0) } )
294
+ . max ( )
295
+ {
296
+ return replacement
297
+ }
298
+
299
+ // Try latest stable as fallback, but only if there are stable toolchains
300
+ let stableToolchains = config. installedToolchains. filter { $0. isStableRelease ( ) && !excluding. contains ( $0) }
301
+ if !stableToolchains. isEmpty {
302
+ return stableToolchains. max ( )
303
+ }
304
+
305
+ // Finally, try any remaining toolchain
306
+ return config. installedToolchains. filter { !excluding. contains ( $0) } . max ( )
307
+ }
308
+
309
+ private func displayProgress( _ ctx: SwiftlyCoreContext , index: Int , total: Int , toolchain: ToolchainVersion ) async {
310
+ if total > 1 {
311
+ await ctx. message ( " [ \( index + 1 ) / \( total) ] Processing \( toolchain) " )
312
+ }
313
+ }
314
+
315
+ private func displayCompletionMessage( _ ctx: SwiftlyCoreContext , _ toolchainCount: Int ) async {
136
316
await ctx. message ( )
137
- await ctx. message ( " \( toolchains. count) toolchain(s) successfully uninstalled " )
317
+ if self . toolchains. count == 1 {
318
+ await ctx. message ( " \( toolchainCount) toolchain(s) successfully uninstalled " )
319
+ } else {
320
+ await ctx. message ( " Successfully uninstalled \( toolchainCount) toolchain(s) from \( self . toolchains. count) selector(s) " )
321
+ }
138
322
}
139
323
140
324
static func execute(
0 commit comments