@@ -6,11 +6,24 @@ struct Uninstall: SwiftlyCommand {
66 abstract: " Remove an installed toolchain. "
77 )
88
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+
922 @Argument ( help: ArgumentHelp (
1023 " The toolchain(s) to uninstall. " ,
1124 discussion: """
1225
13- The toolchain selector provided determines which toolchains to uninstall. Specific \
26+ The list of toolchain selectors determines which toolchains to uninstall. Specific \
1427 toolchains can be uninstalled by using their full names as the selector, for example \
1528 a full stable release version with patch (a.b.c):
1629
@@ -20,6 +33,10 @@ struct Uninstall: SwiftlyCommand {
2033
2134 $ swiftly uninstall 5.7-snapshot-2022-06-20
2235
36+ Multiple toolchain selectors can uninstall multiple toolchains at once:
37+
38+ $ swiftly uninstall 5.2.1 6.0.1
39+
2340 Less specific selectors can be used to uninstall multiple toolchains at once. For instance, \
2441 the patch version can be omitted to uninstall all toolchains associated with a given minor version release:
2542
@@ -39,7 +56,7 @@ struct Uninstall: SwiftlyCommand {
3956 $ swiftly uninstall all
4057 """
4158 ) )
42- var toolchain : String
59+ var toolchains : [ String ]
4360
4461 @OptionGroup var root : GlobalOptions
4562
@@ -54,87 +71,254 @@ struct Uninstall: SwiftlyCommand {
5471 }
5572
5673 let startingConfig = try await Config . load ( ctx)
74+ let selectionResult = try await parseAndValidateToolchainSelectors ( startingConfig)
75+ let confirmedToolchains = try await handleErrorsAndGetConfirmation ( ctx, selectionResult)
5776
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+ }
71104 }
72- toolchains = installedToolchains
73105 }
74106
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)
77142
78143 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 ( )
81150 }
82151
83152 if !self . root. assumeYes {
84- await ctx. message ( " The following toolchains will be uninstalled: " )
153+ try await self . confirmUninstallation ( ctx, toolchains, selectionResult. selectorToToolchains)
154+ }
85155
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 ] = [ ]
89165
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 {
91182 await ctx. message ( " Aborting uninstall " )
92- return
183+ throw UninstallCancelledError ( )
93184 }
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) " )
94219 }
220+ }
95221
222+ private func executeUninstalls(
223+ _ ctx: SwiftlyCoreContext ,
224+ _ toolchains: [ ToolchainVersion ] ,
225+ _ startingConfig: Config
226+ ) async throws {
96227 await ctx. message ( )
97228
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+
99235 var config = try await Config . load ( ctx)
100236
101- // If the in-use toolchain was one of the uninstalled toolchains, use a new toolchain.
102237 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)
131239 }
132240
133241 try await Self . execute ( ctx, toolchain, & config, verbose: self . root. verbose)
134242 }
135243
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 {
136316 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+ }
138322 }
139323
140324 static func execute(
0 commit comments