From fa3641de954350965c07c942a2f832fe7b223682 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 15:28:02 -0500 Subject: [PATCH 01/16] feat: SelfUninstall skeleton --- Sources/Swiftly/SelfUninstall.swift | 50 +++++++++++++++++++++++++++++ Sources/Swiftly/Swiftly.swift | 1 + 2 files changed, 51 insertions(+) create mode 100644 Sources/Swiftly/SelfUninstall.swift diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift new file mode 100644 index 00000000..5bede553 --- /dev/null +++ b/Sources/Swiftly/SelfUninstall.swift @@ -0,0 +1,50 @@ +import Foundation +import ArgumentParser +import SwiftlyCore + + +struct SelfUninstall: SwiftlyCommand { + public static let configuration = CommandConfiguration( + abstract: "Uninstall swiftly itself.", + ) + + @OptionGroup var root: GlobalOptions + + private enum CodingKeys: String, CodingKey { + case root + } + + mutating func run() async throws { + try await self.run(Swiftly.createDefaultContext()) + } + + mutating func run(_ ctx: SwiftlyCoreContext) async throws { + let _ = try await validateSwiftly(ctx) + let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) + let swiftlyHome = Swiftly.currentPlatform.swiftlyHomeDir(ctx) + + guard try await fs.exists(atPath: swiftlyBin) else { + throw SwiftlyError( + message: + "Self uninstall doesn't work when swiftly has been installed externally. Please uninstall it from the source where you installed it in the first place." + ) + } + + let _ = try await Self.execute(ctx, verbose: self.root.verbose) + } + + public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws { + await ctx.print("Uninstalling swiftly...") + + let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) + let swiftlyHome = Swiftly.currentPlatform.swiftlyHomeDir(ctx) + + await ctx.print("Removing swiftly binary from \(swiftlyBin)...") + // try await fs.remove(atPath: swiftlyBin) + + await ctx.print("Removing swiftly home directory from \(swiftlyHome)...") + // try await fs.remove(atPath: swiftlyHome) + + await ctx.print("Swiftly uninstalled successfully.") + } +} diff --git a/Sources/Swiftly/Swiftly.swift b/Sources/Swiftly/Swiftly.swift index 4ef95b7a..f8dadc72 100644 --- a/Sources/Swiftly/Swiftly.swift +++ b/Sources/Swiftly/Swiftly.swift @@ -46,6 +46,7 @@ public struct Swiftly: SwiftlyCommand { Init.self, SelfUpdate.self, Run.self, + SelfUninstall.self, Link.self, Unlink.self, ] From a829f89fc25b9ffcf7d0b34730a443ce9483fe16 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 15:33:33 -0500 Subject: [PATCH 02/16] feat: prompt irreversible action confirmation --- Sources/Swiftly/SelfUninstall.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index 5bede553..effcf596 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -30,10 +30,19 @@ struct SelfUninstall: SwiftlyCommand { ) } - let _ = try await Self.execute(ctx, verbose: self.root.verbose) + try await Self.execute(ctx, verbose: self.root.verbose) } public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws { + await ctx.print(""" + You are about to uninstall swiftly. + This will remove the swiftly binary and all the files in the swiftly home directory. + This action is irreversible. + """) + + guard await ctx.promptForConfirmation(defaultBehavior: true) else { + throw SwiftlyError(message: "swiftly installation has been cancelled") + } await ctx.print("Uninstalling swiftly...") let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) From 319ba2384ed4b77ca0fdefe44cc11fad5ff47baf Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 15:51:06 -0500 Subject: [PATCH 03/16] test: add preliminary tests for self-uninstall --- Tests/SwiftlyTests/SelfUninstallTests.swift | 26 +++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Tests/SwiftlyTests/SelfUninstallTests.swift diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift new file mode 100644 index 00000000..3ca67891 --- /dev/null +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -0,0 +1,26 @@ +import Foundation +import Testing +@testable import Swiftly +@testable import SwiftlyCore + +@Suite struct SelfUninstallTests { + // Test that swiftly uninstall successfully removes the swiftly binary and the bin directory + @Test(.mockedSwiftlyVersion()) func uninstall() async throws { + try await SwiftlyTests.withTestHome { + let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx) + let swiftlyHomeDir = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) + + try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) + + #expect( + try await fs.exists(atPath: swiftlyBinDir) == false, + "swiftly bin directory should be removed" + ) + #expect( + try await fs.exists(atPath: swiftlyHomeDir) == false, + "swiftly home directory should be removed" + ) + + } + } +} \ No newline at end of file From 874391b2abbb4135364b43b69c0ca38fd72fb6bd Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 16:32:49 -0500 Subject: [PATCH 04/16] test: rename test --- Tests/SwiftlyTests/SelfUninstallTests.swift | 58 +++++++++++++++++++-- 1 file changed, 53 insertions(+), 5 deletions(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 3ca67891..6a50a0dc 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -1,12 +1,13 @@ import Foundation -import Testing @testable import Swiftly @testable import SwiftlyCore +import SystemPackage +import Testing @Suite struct SelfUninstallTests { // Test that swiftly uninstall successfully removes the swiftly binary and the bin directory - @Test(.mockedSwiftlyVersion()) func uninstall() async throws { - try await SwiftlyTests.withTestHome { + @Test(.mockedSwiftlyVersion()) func removesHomeAndBinDir() async throws { + try await SwiftlyTests.withTestHome { let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx) let swiftlyHomeDir = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) @@ -20,7 +21,54 @@ import Testing try await fs.exists(atPath: swiftlyHomeDir) == false, "swiftly home directory should be removed" ) - } } -} \ No newline at end of file + + // @Test(.mockedSwiftlyVersion(), .testHome(), arguments: [ + // "/bin/bash", + // "/bin/zsh", + // "/bin/fish", + // ]) func removesEntryFromShell(_ shell: String) async throws { + // var ctx = SwiftlyTests.ctx + // ctx.mockedShell = shell + + // try await SwiftlyTests.$ctx.withValue(ctx) { + // let envScript: FilePath? + // if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { + // envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) / "env.sh" + // } else if shell.hasSuffix("fish") { + // envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) / "env.fish" + // } else { + // envScript = nil + // } + + // // if let envScript { + // // print(envScript.string) + // // } + + // // WHEN: swiftly is invoked to uninstall + // try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) + + // // AND: it removes the source line from the user profile + // // var sourceLineExist = false + // for p in [ + // ".profile", + // ".zprofile", + // ".bash_profile", + // ".bash_login", + // ".config/fish/conf.d/swiftly.fish", + // ] { + // let profile = SwiftlyTests.ctx.mockedHomeDir! / p + // if try await fs.exists(atPath: profile) { + // // print profile contents only + // if let profileContents = try? String(contentsOf: profile) { + // print("contents of profile \(profileContents)") + // // sourceLineExist = profileContents.contains(envScript.string) + // } + + // } + // } + // // #expect(sourceLineExist == false, "source line should be removed from the profile") + // } + // } +} From 30af030bc795fab25a2536b3f10e730f86ffb4f8 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 16:32:55 -0500 Subject: [PATCH 05/16] chore: lint --- Sources/Swiftly/SelfUninstall.swift | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index effcf596..0ce27572 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -1,8 +1,7 @@ -import Foundation import ArgumentParser +import Foundation import SwiftlyCore - struct SelfUninstall: SwiftlyCommand { public static let configuration = CommandConfiguration( abstract: "Uninstall swiftly itself.", @@ -21,7 +20,6 @@ struct SelfUninstall: SwiftlyCommand { mutating func run(_ ctx: SwiftlyCoreContext) async throws { let _ = try await validateSwiftly(ctx) let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) - let swiftlyHome = Swiftly.currentPlatform.swiftlyHomeDir(ctx) guard try await fs.exists(atPath: swiftlyBin) else { throw SwiftlyError( @@ -33,7 +31,7 @@ struct SelfUninstall: SwiftlyCommand { try await Self.execute(ctx, verbose: self.root.verbose) } - public static func execute(_ ctx: SwiftlyCoreContext, verbose: Bool) async throws { + public static func execute(_ ctx: SwiftlyCoreContext, verbose _: Bool) async throws { await ctx.print(""" You are about to uninstall swiftly. This will remove the swiftly binary and all the files in the swiftly home directory. @@ -49,10 +47,10 @@ struct SelfUninstall: SwiftlyCommand { let swiftlyHome = Swiftly.currentPlatform.swiftlyHomeDir(ctx) await ctx.print("Removing swiftly binary from \(swiftlyBin)...") - // try await fs.remove(atPath: swiftlyBin) + try await fs.remove(atPath: swiftlyBin) await ctx.print("Removing swiftly home directory from \(swiftlyHome)...") - // try await fs.remove(atPath: swiftlyHome) + try await fs.remove(atPath: swiftlyHome) await ctx.print("Swiftly uninstalled successfully.") } From 98f9a58b4d85a1de0483d63bdb1fca57e0823ad5 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 18:24:31 -0500 Subject: [PATCH 06/16] test: add expects & skeleton for more --- Tests/SwiftlyTests/SelfUninstallTests.swift | 62 +++++---------------- 1 file changed, 15 insertions(+), 47 deletions(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 6a50a0dc..a9b4a255 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -10,6 +10,14 @@ import Testing try await SwiftlyTests.withTestHome { let swiftlyBinDir = Swiftly.currentPlatform.swiftlyBinDir(SwiftlyTests.ctx) let swiftlyHomeDir = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) + #expect( + try await fs.exists(atPath: swiftlyBinDir) == true, + "swiftly bin directory should exist" + ) + #expect( + try await fs.exists(atPath: swiftlyHomeDir) == true, + "swiftly home directory should exist" + ) try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) @@ -24,51 +32,11 @@ import Testing } } - // @Test(.mockedSwiftlyVersion(), .testHome(), arguments: [ - // "/bin/bash", - // "/bin/zsh", - // "/bin/fish", - // ]) func removesEntryFromShell(_ shell: String) async throws { - // var ctx = SwiftlyTests.ctx - // ctx.mockedShell = shell - - // try await SwiftlyTests.$ctx.withValue(ctx) { - // let envScript: FilePath? - // if shell.hasSuffix("bash") || shell.hasSuffix("zsh") { - // envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) / "env.sh" - // } else if shell.hasSuffix("fish") { - // envScript = Swiftly.currentPlatform.swiftlyHomeDir(SwiftlyTests.ctx) / "env.fish" - // } else { - // envScript = nil - // } - - // // if let envScript { - // // print(envScript.string) - // // } - - // // WHEN: swiftly is invoked to uninstall - // try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) - - // // AND: it removes the source line from the user profile - // // var sourceLineExist = false - // for p in [ - // ".profile", - // ".zprofile", - // ".bash_profile", - // ".bash_login", - // ".config/fish/conf.d/swiftly.fish", - // ] { - // let profile = SwiftlyTests.ctx.mockedHomeDir! / p - // if try await fs.exists(atPath: profile) { - // // print profile contents only - // if let profileContents = try? String(contentsOf: profile) { - // print("contents of profile \(profileContents)") - // // sourceLineExist = profileContents.contains(envScript.string) - // } - - // } - // } - // // #expect(sourceLineExist == false, "source line should be removed from the profile") - // } - // } + @Test(.testHome(), arguments: [ + "/bin/bash", + "/bin/zsh", + "/bin/fish", + ]) func removesEntryFromShellProfile(_ shell: String) async throws { + #expect(true) + } } From e1e1444838d17f27f223aff5c56207f4d8186bb9 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Mon, 5 May 2025 19:29:48 -0500 Subject: [PATCH 07/16] chore: lint --- Tests/SwiftlyTests/SelfUninstallTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index a9b4a255..73f2aa8d 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -36,7 +36,7 @@ import Testing "/bin/bash", "/bin/zsh", "/bin/fish", - ]) func removesEntryFromShellProfile(_ shell: String) async throws { + ]) func removesEntryFromShellProfile(_: String) async throws { #expect(true) } } From 3bd901d50a127db2d13ea90b8ee4a2dd5457b8c2 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 19:39:06 -0500 Subject: [PATCH 08/16] feat: remove sourceLine from shell profile --- Sources/Swiftly/SelfUninstall.swift | 59 +++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index 0ce27572..4ade4b08 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -1,6 +1,7 @@ import ArgumentParser import Foundation import SwiftlyCore +import SystemPackage struct SelfUninstall: SwiftlyCommand { public static let configuration = CommandConfiguration( @@ -43,6 +44,64 @@ struct SelfUninstall: SwiftlyCommand { } await ctx.print("Uninstalling swiftly...") + let shell = if let mockedShell = ctx.mockedShell { + mockedShell + } else { + if let s = ProcessInfo.processInfo.environment["SHELL"] { + s + } else { + try await Swiftly.currentPlatform.getShell() + } + } + + let envFile: FilePath + let sourceLine: String + if shell.hasSuffix("fish") { + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx) / "env.fish" + sourceLine = """ + + # Added by swiftly + source "\(envFile)" + """ + } else { + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx) / "env.sh" + sourceLine = """ + + # Added by swiftly + . "\(envFile)" + """ + } + + let userHome = ctx.mockedHomeDir ?? fs.home + + let profileHome: FilePath + if shell.hasSuffix("zsh") { + profileHome = userHome / ".zprofile" + } else if shell.hasSuffix("bash") { + if case let p = userHome / ".bash_profile", try await fs.exists(atPath: p) { + profileHome = p + } else if case let p = userHome / ".bash_login", try await fs.exists(atPath: p) { + profileHome = p + } else { + profileHome = userHome / ".profile" + } + } else if shell.hasSuffix("fish") { + if let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], case let xdgConfigURL = FilePath(xdgConfigHome) { + profileHome = xdgConfigURL / "fish/conf.d/swiftly.fish" + } else { + profileHome = userHome / ".config/fish/conf.d/swiftly.fish" + } + } else { + profileHome = userHome / ".profile" + } + + await ctx.print("Removing swiftly from shell profile at \(profileHome)...") + + if case let profileContents = try String(contentsOf: profileHome, encoding: .utf8), profileContents.contains(sourceLine) { + let newContents = profileContents.replacingOccurrences(of: sourceLine, with: "") + try Data(newContents.utf8).write(to: profileHome, options: [.atomic]) + } + let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) let swiftlyHome = Swiftly.currentPlatform.swiftlyHomeDir(ctx) From 6007d9749b0adb8d11e1fa8c27131d770367cff4 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 20:56:02 -0500 Subject: [PATCH 09/16] test: removesEntryFromShellProfile tests --- Tests/SwiftlyTests/SelfUninstallTests.swift | 80 ++++++++++++++++++++- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 73f2aa8d..6a147a52 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -32,11 +32,85 @@ import Testing } } - @Test(.testHome(), arguments: [ + @Test(.mockedSwiftlyVersion(), .testHome(), arguments: [ "/bin/bash", "/bin/zsh", "/bin/fish", - ]) func removesEntryFromShellProfile(_: String) async throws { - #expect(true) + ]) func removesEntryFromShellProfile(_ shell: String) async throws { + var ctx = SwiftlyTests.ctx + ctx.mockedShell = shell + + try await SwiftlyTests.$ctx.withValue(ctx) { + // Create a profile file with the source line + let userHome = SwiftlyTests.ctx.mockedHomeDir! + + let profileHome: FilePath + if shell.hasSuffix("zsh") { + profileHome = userHome / ".zprofile" + } else if shell.hasSuffix("bash") { + if case let p = userHome / ".bash_profile", try await fs.exists(atPath: p) { + profileHome = p + } else if case let p = userHome / ".bash_login", try await fs.exists(atPath: p) { + profileHome = p + } else { + profileHome = userHome / ".profile" + } + } else if shell.hasSuffix("fish") { + if let xdgConfigHome = ProcessInfo.processInfo.environment["XDG_CONFIG_HOME"], case let xdgConfigURL = FilePath(xdgConfigHome) { + let confDir = xdgConfigURL / "fish/conf.d" + try await fs.mkdir(.parents, atPath: confDir) + profileHome = confDir / "swiftly.fish" + } else { + let confDir = userHome / ".config/fish/conf.d" + try await fs.mkdir(.parents, atPath: confDir) + profileHome = confDir / "swiftly.fish" + } + } else { + profileHome = userHome / ".profile" + } + + let envFile: FilePath + let sourceLine: String + if shell.hasSuffix("fish") { + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx) / "env.fish" + sourceLine = """ + + # Added by swiftly + source "\(envFile)" + """ + } else { + envFile = Swiftly.currentPlatform.swiftlyHomeDir(ctx) / "env.sh" + sourceLine = """ + + # Added by swiftly + . "\(envFile)" + """ + } + + let shellProfileContents = """ + some other line before + \(sourceLine) + some other line after + """ + + try Data(shellProfileContents.utf8).write(to: profileHome) + + // then call swiftly uninstall + try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) + + var sourceLineRemoved = true + for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { + let profile = SwiftlyTests.ctx.mockedHomeDir! / p + if try await fs.exists(atPath: profile) { + if let profileContents = try? String(contentsOf: profile), profileContents.contains(sourceLine) { + // expect only the source line is removed + #expect(profileContents == shellProfileContents.replacingOccurrences(of: sourceLine, with: "")) + sourceLineRemoved = false + break + } + } + } + #expect(sourceLineRemoved, "swiftly should be removed from the profile file") + } } } From 16be142a7828171a373fdf0a8010ea57ce60d182 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 21:04:31 -0500 Subject: [PATCH 10/16] feat: add warning for unisntalling toolchains --- Sources/Swiftly/SelfUninstall.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index 4ade4b08..3c6a4278 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -36,6 +36,7 @@ struct SelfUninstall: SwiftlyCommand { await ctx.print(""" You are about to uninstall swiftly. This will remove the swiftly binary and all the files in the swiftly home directory. + All installed toolchains will not be removed, if you want to remove them, please do so manually with `swiftly uninstall all`. This action is irreversible. """) From b24ad4e59ff166bb13f2e13db0af969159fd6791 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 21:09:52 -0500 Subject: [PATCH 11/16] test: modify shell profile after existence check --- Sources/Swiftly/SelfUninstall.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Sources/Swiftly/SelfUninstall.swift b/Sources/Swiftly/SelfUninstall.swift index 3c6a4278..75c2069d 100644 --- a/Sources/Swiftly/SelfUninstall.swift +++ b/Sources/Swiftly/SelfUninstall.swift @@ -98,9 +98,11 @@ struct SelfUninstall: SwiftlyCommand { await ctx.print("Removing swiftly from shell profile at \(profileHome)...") - if case let profileContents = try String(contentsOf: profileHome, encoding: .utf8), profileContents.contains(sourceLine) { - let newContents = profileContents.replacingOccurrences(of: sourceLine, with: "") - try Data(newContents.utf8).write(to: profileHome, options: [.atomic]) + if try await fs.exists(atPath: profileHome) { + if case let profileContents = try String(contentsOf: profileHome, encoding: .utf8), profileContents.contains(sourceLine) { + let newContents = profileContents.replacingOccurrences(of: sourceLine, with: "") + try Data(newContents.utf8).write(to: profileHome, options: [.atomic]) + } } let swiftlyBin = Swiftly.currentPlatform.swiftlyBinDir(ctx) From b4594d3a557d543f1b22bcd25db407e5c7747bf0 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 21:14:52 -0500 Subject: [PATCH 12/16] doc: update self-uninstall in README --- README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a8f6991c..809c61dd 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,12 @@ This command checks to see if there are new versions of swiftly itself and upgra ## Uninstalling swiftly -Currently, only manual uninstallation is supported. If you need to uninstall swiftly, please follow the instructions below: +swiftly can be savely removed with the following command: + +`swiftly self-uninstall` + +
+If you want to do so manually, please follow the instructions below: NOTE: This will not uninstall any toolchains you have installed unless you do so manually with `swiftly uninstall all`. @@ -76,6 +81,8 @@ NOTE: This will not uninstall any toolchains you have installed unless you do so 4. Restart your shell and check you have correctly removed the swiftly environment. +
+ ## Contributing Welcome to the Swift community! From fab0d6978473eeec557a89018234bcde511687e3 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 21:28:20 -0500 Subject: [PATCH 13/16] doc: generate docc reference for self-uninstall --- .../SwiftlyDocs.docc/swiftly-cli-reference.md | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md index fc2fb289..9b354670 100644 --- a/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md +++ b/Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md @@ -545,6 +545,36 @@ The script will receive the argument '+abcde' followed by '+xyz'. +## self-uninstall + +Uninstall swiftly itself. + +``` +swiftly self-uninstall [--assume-yes] [--verbose] [--version] [--help] +``` + +**--assume-yes:** + +*Disable confirmation prompts by assuming 'yes'* + + +**--verbose:** + +*Enable verbose reporting from swiftly* + + +**--version:** + +*Show the version.* + + +**--help:** + +*Show help information.* + + + + ## link Link swiftly so it resumes management of the active toolchain. From 3f619a25304327d5f8ed4c20fad6f2644dc027b6 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 22:32:18 -0500 Subject: [PATCH 14/16] test: add check for shell profile existence --- Tests/SwiftlyTests/SelfUninstallTests.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 6a147a52..53bca7df 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -95,6 +95,11 @@ import Testing try Data(shellProfileContents.utf8).write(to: profileHome) + #expect( + try await fs.exists(atPath: profileHome) == true, + "shell profile file should exist" + ) + // then call swiftly uninstall try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) @@ -111,6 +116,10 @@ import Testing } } #expect(sourceLineRemoved, "swiftly should be removed from the profile file") + #expect( + try await fs.exists(atPath: profileHome) == true, + "shell profile file should still exist" + ) } } } From 6da4d8421f6b2a3c600fb6ec2756c3a7d94adc33 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Tue, 6 May 2025 22:37:37 -0500 Subject: [PATCH 15/16] test: move expect up --- Tests/SwiftlyTests/SelfUninstallTests.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 53bca7df..6dadd150 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -103,6 +103,11 @@ import Testing // then call swiftly uninstall try await SwiftlyTests.runCommand(SelfUninstall.self, ["self-uninstall"]) + #expect( + try await fs.exists(atPath: profileHome) == true, + "shell profile file should still exist" + ) + var sourceLineRemoved = true for p in [".profile", ".zprofile", ".bash_profile", ".bash_login", ".config/fish/conf.d/swiftly.fish"] { let profile = SwiftlyTests.ctx.mockedHomeDir! / p @@ -116,10 +121,6 @@ import Testing } } #expect(sourceLineRemoved, "swiftly should be removed from the profile file") - #expect( - try await fs.exists(atPath: profileHome) == true, - "shell profile file should still exist" - ) } } } From 8c01659719aaecfaa3fe3ea32e7e9c6ec60306c7 Mon Sep 17 00:00:00 2001 From: Louis Qian Date: Wed, 7 May 2025 01:23:26 -0500 Subject: [PATCH 16/16] test: add expect comment --- Tests/SwiftlyTests/SelfUninstallTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/SwiftlyTests/SelfUninstallTests.swift b/Tests/SwiftlyTests/SelfUninstallTests.swift index 6dadd150..738c9d16 100644 --- a/Tests/SwiftlyTests/SelfUninstallTests.swift +++ b/Tests/SwiftlyTests/SelfUninstallTests.swift @@ -114,7 +114,10 @@ import Testing if try await fs.exists(atPath: profile) { if let profileContents = try? String(contentsOf: profile), profileContents.contains(sourceLine) { // expect only the source line is removed - #expect(profileContents == shellProfileContents.replacingOccurrences(of: sourceLine, with: "")) + #expect( + profileContents == shellProfileContents.replacingOccurrences(of: sourceLine, with: ""), + "the original profile contents should not be changed" + ) sourceLineRemoved = false break }